From c32722d24e78be0b23857f1741ddba0b12e34ecd Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Tue, 3 Sep 2024 11:42:18 +0200 Subject: [PATCH 1/4] tests(app): tests restored --- app/.eslintrc.js | 13 + .../async-storage.js | 4 + app/__testHelpers__/createContextBaseTests.js | 52 + app/__testHelpers__/expectThrowAsync.js | 16 + app/__testHelpers__/getInvalidIntegers.js | 8 + app/__testHelpers__/getInvalidNumbers.js | 13 + app/__testHelpers__/mockCall.js | 13 + app/__testHelpers__/mockCollection.js | 24 + app/__testHelpers__/simpleRandom.js | 3 + app/__testHelpers__/stub.js | 37 + .../analytics/getDeviceData.tests.js | 5 + .../components/ActionButton.tests.js | 5 + app/__tests__/components/BackButton.tests.js | 5 + app/__tests__/components/CatchErrors.tests.js | 5 + .../components/CharacterInput.tests.js | 5 + app/__tests__/components/Checkbox.tests.js | 5 + app/__tests__/components/Confirm.tests.js | 5 + app/__tests__/components/Connection.tests.js | 5 + .../components/ErrorMessage.tests.js | 5 + app/__tests__/components/FadePanel.tests.js | 5 + app/__tests__/components/LeaButton.tests.js | 5 + .../components/LeaButtonGroup.tests.js | 5 + app/__tests__/components/LeaText.tests.js | 5 + app/__tests__/components/Loading.tests.js | 5 + .../components/MarkdownWithTts.tests.js | 5 + .../components/NullComponent.tests.js | 5 + .../components/ProfileButton.tests.js | 5 + app/__tests__/components/RouteButton.tests.js | 5 + app/__tests__/components/SoundIcon.tests.js | 5 + .../components/ViewContainer.tests.js | 5 + .../UnitContentElementFactory.tests.js | 53 + .../createRoutableComponent.tests.js | 5 + .../components/images/LeaLogo.tests.js | 5 + app/__tests__/components/layout/Fill.tests.js | 5 + .../components/progress/CurrentProgress.js | 5 + .../components/progress/Diamond.tests.js | 5 + .../progress/StaticCircularProgress.tests.js | 5 + .../progress/computeProgress.tests.js | 28 + .../progress/correctDiamondProgress.tests.js | 34 + .../renderer/media/ImageRenderer.tests.js | 5 + .../renderer/text/MarkdownRenderer.tests.js | 5 + .../renderer/text/PlainTextRenderer.tests.js | 5 + app/__tests__/components/tts.test.js | 359 +++ app/__tests__/contexts/Achievements.tests.js | 6 + app/__tests__/contexts/Dimension.tests.js | 6 + app/__tests__/contexts/Feedback.tests.js | 6 + app/__tests__/contexts/Field.tests.js | 6 + app/__tests__/contexts/Legal.tests.js | 6 + app/__tests__/contexts/Level.tests.js | 6 + app/__tests__/contexts/MapIcons.tests.js | 62 + app/__tests__/contexts/Order.tests.js | 6 + app/__tests__/contexts/Sync.tests.js | 228 ++ app/__tests__/contexts/UserProgress.tests.js | 197 ++ .../__snapshots__/MapIcons.tests.js.snap | 40 + .../contexts/createContextStorage.tests.js | 59 + app/__tests__/env/Config.tests.js | 5 + app/__tests__/env/Sound.tests.js | 5 + .../env/loadSettingsFromUserProfile.tests.js | 5 + app/__tests__/errors/ErrorReporter.tests.js | 8 + app/__tests__/errors/normalizeError.tests.js | 8 + app/__tests__/hooks/useBackHandler.tests.js | 5 + app/__tests__/hooks/useConnection.tests.js | 5 + .../useKeyboardVisibilityHandler.tests.js | 5 + app/__tests__/hooks/useLogin.tests.js | 5 + app/__tests__/hooks/useProgress.tests.js | 5 + .../hooks/useScreenIsActive.tests.js | 5 + app/__tests__/hooks/useTimeout.tests.js | 5 + app/__tests__/hooks/useUser.tests.js | 5 + app/__tests__/hooks/useVoices.tests.js | 5 + app/__tests__/i18n/i18n.test.js | 28 + .../infrastructure/app/AppTemrinate.tests.js | 5 + .../collections/collection.tests.js | 5 + .../infrastructure/createRepository.tests.js | 17 + .../factories/createCollection.tests.js | 5 + app/__tests__/infrastructure/log/log.tests.js | 5 + app/__tests__/items/choice/Choice.tests.js | 190 ++ .../items/choice/ChoiceRenderer.tests.js | 5 + .../items/choice/ScoreChoice.tests.js | 5 + .../choice/getChoiceEntryScoreColor.tests.js | 5 + .../items/cloze/ClozeRenderer.tests.js | 5 + .../items/cloze/Clozetokenizer.tests.js | 691 +++++ .../createScoringSummaryForInput.tests.js | 52 + app/__tests__/items/cloze/scoreCloze.tests.js | 146 ++ app/__tests__/items/connect/Connect.tests.js | 5 + .../connect/ConnectItemRenderer.tests.js | 5 + .../items/connect/ScoreConnect.tests.js | 5 + .../items/highlight/Highlight.tests.js | 5 + .../highlight/HighlightRenderer.tests.js | 5 + .../highlight/HighlightTokenizer.tests.js | 5 + .../items/highlight/scoreHighlight.tests.js | 5 + ...getCompareValuesForSelectableItems.test.js | 22 + .../items/utils/CompareState.tests.js | 41 + .../items/utils/KeyboardTypes.tests.js | 13 + app/__tests__/schema/schema.tests.js | 29 + app/__tests__/schema/settingsSchema.tests.js | 10 + .../schema/validateSettingsSchema.tests.js | 7 + app/__tests__/scoring/Scoring.tests.js | 48 + app/__tests__/scoring/getScoring.tests.js | 5 + app/__tests__/screens/BaseScreen.tests.js | 5 + .../screens/complete/Celebrate.tests.js | 5 + .../screens/complete/CompleteScreen.tests.js | 5 + .../complete/generateFeedback.tests.js | 48 + .../complete/loadCompleteData.tests.js | 5 + .../screens/home/HomeScreen.tests.js | 5 + .../screens/home/laodHomeData.tests.js | 5 + .../screens/map/loadMapData.tests.js | 261 ++ .../startup/createSessionValidator.tests.js | 5 + app/__tests__/startup/initAppSession.tests.js | 5 + app/__tests__/startup/initContexts.tests.js | 5 + .../startup/initExceptionHandling.tests.js | 5 + app/__tests__/startup/initSound.tests.js | 5 + app/__tests__/startup/initTts.tests.js | 5 + app/__tests__/state/AppSession.tests.js | 5 + .../styles/createStyleSheet.tests.js | 5 + app/__tests__/styles/makeTransparent.tests.js | 5 + app/__tests__/styles/mergeStyles.tests.js | 5 + app/__tests__/tts/TTSSpeedConfig.tests.js | 5 + app/__tests__/tts/TTSVoiceConfig.tests.js | 5 + app/__tests__/utils/array/byDocId.tests.js | 17 + .../utils/array/byOrderedIds.tests.js | 33 + .../utils/array/randomArrayElement.tests.js | 32 + .../utils/array/toArrayIfNot.tests.js | 20 + app/__tests__/utils/array/toDocId.tests.js | 8 + .../utils/createTimesPromise.tests.js | 43 + app/__tests__/utils/isOS.tests.js | 5 + app/__tests__/utils/math/average.tests.js | 16 + .../utils/math/getPositionOnCircle.tests.js | 21 + .../utils/math/randomInclusive.tests.js | 14 + .../utils/number/isSafeInteger.tests.js | 17 + .../utils/number/isValidNumber.tests.js | 11 + app/__tests__/utils/number/toInteger.tests.js | 16 + .../utils/object/clearObject.tests.js | 17 + .../utils/object/hasOwnProps.tests.js | 14 + app/__tests__/utils/object/isDefined.tests.js | 10 + .../utils/text/createSimpleTokenizer.tests.js | 12 + app/__tests__/utils/text/isWord.tests.js | 10 + app/babel.config.js | 8 +- app/build.js | 14 +- app/jest.config.js | 10 + app/jestSetup.js | 3 + app/lib/components/BackButton.js | 2 +- app/lib/components/animated/DashedLine.js | 16 +- .../renderer/media/ImageRenderer.js | 2 +- app/lib/contexts/MapIcons.js | 15 +- app/lib/hooks/useConnection.js | 2 +- app/lib/hooks/useDevelopment.js | 2 +- app/lib/i18n.js | 6 +- .../cloze/createScoringSummaryForInput.js | 2 +- app/lib/items/connect/ConnectItemRenderer.js | 1 - app/lib/screens/auth/RegistrationScreen.js | 2 +- app/lib/screens/map/components/Milestone.js | 2 +- app/lib/screens/map/loadMapIcons.js | 20 - app/lib/screens/map/loadProgressData.js | 1 - .../renderer/InstructionsGraphicsRenderer.js | 4 +- app/package-lock.json | 2270 ++++++++++++++++- app/package.json | 14 +- app/settings/settings.json | 16 +- 157 files changed, 5819 insertions(+), 188 deletions(-) create mode 100644 app/.eslintrc.js create mode 100644 app/__mocks__/@react-native-async-storage/async-storage.js create mode 100644 app/__testHelpers__/createContextBaseTests.js create mode 100644 app/__testHelpers__/expectThrowAsync.js create mode 100644 app/__testHelpers__/getInvalidIntegers.js create mode 100644 app/__testHelpers__/getInvalidNumbers.js create mode 100644 app/__testHelpers__/mockCall.js create mode 100644 app/__testHelpers__/mockCollection.js create mode 100644 app/__testHelpers__/simpleRandom.js create mode 100644 app/__testHelpers__/stub.js create mode 100644 app/__tests__/analytics/getDeviceData.tests.js create mode 100644 app/__tests__/components/ActionButton.tests.js create mode 100644 app/__tests__/components/BackButton.tests.js create mode 100644 app/__tests__/components/CatchErrors.tests.js create mode 100644 app/__tests__/components/CharacterInput.tests.js create mode 100644 app/__tests__/components/Checkbox.tests.js create mode 100644 app/__tests__/components/Confirm.tests.js create mode 100644 app/__tests__/components/Connection.tests.js create mode 100644 app/__tests__/components/ErrorMessage.tests.js create mode 100644 app/__tests__/components/FadePanel.tests.js create mode 100644 app/__tests__/components/LeaButton.tests.js create mode 100644 app/__tests__/components/LeaButtonGroup.tests.js create mode 100644 app/__tests__/components/LeaText.tests.js create mode 100644 app/__tests__/components/Loading.tests.js create mode 100644 app/__tests__/components/MarkdownWithTts.tests.js create mode 100644 app/__tests__/components/NullComponent.tests.js create mode 100644 app/__tests__/components/ProfileButton.tests.js create mode 100644 app/__tests__/components/RouteButton.tests.js create mode 100644 app/__tests__/components/SoundIcon.tests.js create mode 100644 app/__tests__/components/ViewContainer.tests.js create mode 100644 app/__tests__/components/factories/UnitContentElementFactory.tests.js create mode 100644 app/__tests__/components/factories/createRoutableComponent.tests.js create mode 100644 app/__tests__/components/images/LeaLogo.tests.js create mode 100644 app/__tests__/components/layout/Fill.tests.js create mode 100644 app/__tests__/components/progress/CurrentProgress.js create mode 100644 app/__tests__/components/progress/Diamond.tests.js create mode 100644 app/__tests__/components/progress/StaticCircularProgress.tests.js create mode 100644 app/__tests__/components/progress/computeProgress.tests.js create mode 100644 app/__tests__/components/progress/correctDiamondProgress.tests.js create mode 100644 app/__tests__/components/renderer/media/ImageRenderer.tests.js create mode 100644 app/__tests__/components/renderer/text/MarkdownRenderer.tests.js create mode 100644 app/__tests__/components/renderer/text/PlainTextRenderer.tests.js create mode 100644 app/__tests__/components/tts.test.js create mode 100644 app/__tests__/contexts/Achievements.tests.js create mode 100644 app/__tests__/contexts/Dimension.tests.js create mode 100644 app/__tests__/contexts/Feedback.tests.js create mode 100644 app/__tests__/contexts/Field.tests.js create mode 100644 app/__tests__/contexts/Legal.tests.js create mode 100644 app/__tests__/contexts/Level.tests.js create mode 100644 app/__tests__/contexts/MapIcons.tests.js create mode 100644 app/__tests__/contexts/Order.tests.js create mode 100644 app/__tests__/contexts/Sync.tests.js create mode 100644 app/__tests__/contexts/UserProgress.tests.js create mode 100644 app/__tests__/contexts/__snapshots__/MapIcons.tests.js.snap create mode 100644 app/__tests__/contexts/createContextStorage.tests.js create mode 100644 app/__tests__/env/Config.tests.js create mode 100644 app/__tests__/env/Sound.tests.js create mode 100644 app/__tests__/env/loadSettingsFromUserProfile.tests.js create mode 100644 app/__tests__/errors/ErrorReporter.tests.js create mode 100644 app/__tests__/errors/normalizeError.tests.js create mode 100644 app/__tests__/hooks/useBackHandler.tests.js create mode 100644 app/__tests__/hooks/useConnection.tests.js create mode 100644 app/__tests__/hooks/useKeyboardVisibilityHandler.tests.js create mode 100644 app/__tests__/hooks/useLogin.tests.js create mode 100644 app/__tests__/hooks/useProgress.tests.js create mode 100644 app/__tests__/hooks/useScreenIsActive.tests.js create mode 100644 app/__tests__/hooks/useTimeout.tests.js create mode 100644 app/__tests__/hooks/useUser.tests.js create mode 100644 app/__tests__/hooks/useVoices.tests.js create mode 100644 app/__tests__/i18n/i18n.test.js create mode 100644 app/__tests__/infrastructure/app/AppTemrinate.tests.js create mode 100644 app/__tests__/infrastructure/collections/collection.tests.js create mode 100644 app/__tests__/infrastructure/createRepository.tests.js create mode 100644 app/__tests__/infrastructure/factories/createCollection.tests.js create mode 100644 app/__tests__/infrastructure/log/log.tests.js create mode 100644 app/__tests__/items/choice/Choice.tests.js create mode 100644 app/__tests__/items/choice/ChoiceRenderer.tests.js create mode 100644 app/__tests__/items/choice/ScoreChoice.tests.js create mode 100644 app/__tests__/items/choice/getChoiceEntryScoreColor.tests.js create mode 100644 app/__tests__/items/cloze/ClozeRenderer.tests.js create mode 100644 app/__tests__/items/cloze/Clozetokenizer.tests.js create mode 100644 app/__tests__/items/cloze/createScoringSummaryForInput.tests.js create mode 100644 app/__tests__/items/cloze/scoreCloze.tests.js create mode 100644 app/__tests__/items/connect/Connect.tests.js create mode 100644 app/__tests__/items/connect/ConnectItemRenderer.tests.js create mode 100644 app/__tests__/items/connect/ScoreConnect.tests.js create mode 100644 app/__tests__/items/highlight/Highlight.tests.js create mode 100644 app/__tests__/items/highlight/HighlightRenderer.tests.js create mode 100644 app/__tests__/items/highlight/HighlightTokenizer.tests.js create mode 100644 app/__tests__/items/highlight/scoreHighlight.tests.js create mode 100644 app/__tests__/items/shared/getCompareValuesForSelectableItems.test.js create mode 100644 app/__tests__/items/utils/CompareState.tests.js create mode 100644 app/__tests__/items/utils/KeyboardTypes.tests.js create mode 100644 app/__tests__/schema/schema.tests.js create mode 100644 app/__tests__/schema/settingsSchema.tests.js create mode 100644 app/__tests__/schema/validateSettingsSchema.tests.js create mode 100644 app/__tests__/scoring/Scoring.tests.js create mode 100644 app/__tests__/scoring/getScoring.tests.js create mode 100644 app/__tests__/screens/BaseScreen.tests.js create mode 100644 app/__tests__/screens/complete/Celebrate.tests.js create mode 100644 app/__tests__/screens/complete/CompleteScreen.tests.js create mode 100644 app/__tests__/screens/complete/generateFeedback.tests.js create mode 100644 app/__tests__/screens/complete/loadCompleteData.tests.js create mode 100644 app/__tests__/screens/home/HomeScreen.tests.js create mode 100644 app/__tests__/screens/home/laodHomeData.tests.js create mode 100644 app/__tests__/screens/map/loadMapData.tests.js create mode 100644 app/__tests__/startup/createSessionValidator.tests.js create mode 100644 app/__tests__/startup/initAppSession.tests.js create mode 100644 app/__tests__/startup/initContexts.tests.js create mode 100644 app/__tests__/startup/initExceptionHandling.tests.js create mode 100644 app/__tests__/startup/initSound.tests.js create mode 100644 app/__tests__/startup/initTts.tests.js create mode 100644 app/__tests__/state/AppSession.tests.js create mode 100644 app/__tests__/styles/createStyleSheet.tests.js create mode 100644 app/__tests__/styles/makeTransparent.tests.js create mode 100644 app/__tests__/styles/mergeStyles.tests.js create mode 100644 app/__tests__/tts/TTSSpeedConfig.tests.js create mode 100644 app/__tests__/tts/TTSVoiceConfig.tests.js create mode 100644 app/__tests__/utils/array/byDocId.tests.js create mode 100644 app/__tests__/utils/array/byOrderedIds.tests.js create mode 100644 app/__tests__/utils/array/randomArrayElement.tests.js create mode 100644 app/__tests__/utils/array/toArrayIfNot.tests.js create mode 100644 app/__tests__/utils/array/toDocId.tests.js create mode 100644 app/__tests__/utils/createTimesPromise.tests.js create mode 100644 app/__tests__/utils/isOS.tests.js create mode 100644 app/__tests__/utils/math/average.tests.js create mode 100644 app/__tests__/utils/math/getPositionOnCircle.tests.js create mode 100644 app/__tests__/utils/math/randomInclusive.tests.js create mode 100644 app/__tests__/utils/number/isSafeInteger.tests.js create mode 100644 app/__tests__/utils/number/isValidNumber.tests.js create mode 100644 app/__tests__/utils/number/toInteger.tests.js create mode 100644 app/__tests__/utils/object/clearObject.tests.js create mode 100644 app/__tests__/utils/object/hasOwnProps.tests.js create mode 100644 app/__tests__/utils/object/isDefined.tests.js create mode 100644 app/__tests__/utils/text/createSimpleTokenizer.tests.js create mode 100644 app/__tests__/utils/text/isWord.tests.js create mode 100644 app/jest.config.js create mode 100644 app/jestSetup.js delete mode 100644 app/lib/screens/map/loadMapIcons.js diff --git a/app/.eslintrc.js b/app/.eslintrc.js new file mode 100644 index 00000000..707fbe10 --- /dev/null +++ b/app/.eslintrc.js @@ -0,0 +1,13 @@ +// https://docs.expo.dev/guides/using-eslint/ +module.exports = { + extends: ['expo', 'standard'], + plugins: ['jest'], + env: { + 'jest/globals': true + }, + rules: { + 'react-hooks/exhaustive-deps': 'off', + 'react/display-name': 'off', + 'brace-style': ['error', 'stroustrup', { allowSingleLine: true }] + } +} diff --git a/app/__mocks__/@react-native-async-storage/async-storage.js b/app/__mocks__/@react-native-async-storage/async-storage.js new file mode 100644 index 00000000..27868fbe --- /dev/null +++ b/app/__mocks__/@react-native-async-storage/async-storage.js @@ -0,0 +1,4 @@ +// eslint-disable-next-line +const storageMock = require('@react-native-async-storage/async-storage/jest/async-storage-mock') + +module.exports = storageMock diff --git a/app/__testHelpers__/createContextBaseTests.js b/app/__testHelpers__/createContextBaseTests.js new file mode 100644 index 00000000..1f91d3b7 --- /dev/null +++ b/app/__testHelpers__/createContextBaseTests.js @@ -0,0 +1,52 @@ +import { collectionExists, getCollection } from '../lib/infrastructure/collections/collections' +import { createCollection } from '../lib/infrastructure/createCollection' +import { simpleRandomHex } from '../lib/utils/simpleRandomHex' +import AsyncStorage from '@react-native-async-storage/async-storage/jest/async-storage-mock' + +export const createContextBaseTests = ({ ctx, custom }) => { + const storageKey = `contexts-${ctx.name}` + + beforeAll(() => { + jest.useFakeTimers({ advanceTimers: true }) + }) + + if (ctx.collection) { + describe(ctx.collection.name, () => { + beforeAll(() => { + if (!collectionExists(ctx.name)) { + createCollection({ + name: ctx.name, + isLocal: true + }) + } + }) + + afterAll(() => { + const collection = getCollection(ctx.name) + ctx.collection = () => collection + }) + + it('throws if collection is not initiated', () => { + expect(() => ctx.collection()) + .toThrow(`Collection ${ctx.name} not initialized`) + }) + }) + } + + if (ctx.init) { + describe(ctx.init.name, function () { + it('loads sync docs into collection', async () => { + const _id = simpleRandomHex() + const doc = { _id, title: 'moo' } + await AsyncStorage.setItem(storageKey, JSON.stringify(doc)) + await ctx.init() + const collection = getCollection(ctx.name) + expect(collection.findOne({ _id })) + .toStrictEqual({ _version: 1, ...doc }) + }) + }) + } + if (custom) { + custom() + } +} diff --git a/app/__testHelpers__/expectThrowAsync.js b/app/__testHelpers__/expectThrowAsync.js new file mode 100644 index 00000000..0676c783 --- /dev/null +++ b/app/__testHelpers__/expectThrowAsync.js @@ -0,0 +1,16 @@ +/** + * Test helper to test async functions to throw + * expected errors + * @param fn + * @param message + * @returns {Promise} + */ +export const expectThrowAsync = async ({ fn, message }) => { + try { + await fn() + throw new Error('Expected function to throw an Error') + } + catch (e) { + expect(e.message).toEqual(message) + } +} diff --git a/app/__testHelpers__/getInvalidIntegers.js b/app/__testHelpers__/getInvalidIntegers.js new file mode 100644 index 00000000..b6ae009c --- /dev/null +++ b/app/__testHelpers__/getInvalidIntegers.js @@ -0,0 +1,8 @@ +import { getInvalidNumbers } from './getInvalidNumbers' + +export const getInvalidIntegers = () => getInvalidNumbers().concat([ + -1.1, + 0.2 + 0.1, + Number.MAX_SAFE_INTEGER + 1, + -Number.MAX_SAFE_INTEGER - 1 +]) diff --git a/app/__testHelpers__/getInvalidNumbers.js b/app/__testHelpers__/getInvalidNumbers.js new file mode 100644 index 00000000..6052379d --- /dev/null +++ b/app/__testHelpers__/getInvalidNumbers.js @@ -0,0 +1,13 @@ +export const getInvalidNumbers = () => [ + null, + undefined, + '1', + Number.MAX_VALUE * 2, + -Number.MAX_VALUE * 2, + {}, + NaN, + Infinity, + () => {}, + true, + false +] diff --git a/app/__testHelpers__/mockCall.js b/app/__testHelpers__/mockCall.js new file mode 100644 index 00000000..33054eff --- /dev/null +++ b/app/__testHelpers__/mockCall.js @@ -0,0 +1,13 @@ +import Meteor from '@meteorrn/core' + +export const mockCall = callback => { + const data = { + waitDdpConnected: fn => fn() + } + jest + .spyOn(Meteor, 'getData') + .mockImplementation(() => data) + jest + .spyOn(Meteor, 'call') + .mockImplementation(callback) +} diff --git a/app/__testHelpers__/mockCollection.js b/app/__testHelpers__/mockCollection.js new file mode 100644 index 00000000..a2dfa14c --- /dev/null +++ b/app/__testHelpers__/mockCollection.js @@ -0,0 +1,24 @@ +import { createCollection } from '../lib/infrastructure/createCollection' + +const map = new Map() + +export const mockCollection = (context) => { + if (!map.has(context.name)) { + map.set(context.name, createCollection({ + name: context.name, + isLocal: true + })) + } + const collection = map.get(context.name) + context.collection = () => collection + return context +} + +export const resetCollection = context => context.collection().remove({}) + +export const restoreCollection = context => { + map.delete(context.name) + context.collection = () => { + throw new Error('is not initialized') + } +} diff --git a/app/__testHelpers__/simpleRandom.js b/app/__testHelpers__/simpleRandom.js new file mode 100644 index 00000000..5ac8bd44 --- /dev/null +++ b/app/__testHelpers__/simpleRandom.js @@ -0,0 +1,3 @@ +export const simpleRandom = (size = 6) => [...Array(size)] + .map(() => Math.floor(Math.random() * 16).toString(16)) + .join('') diff --git a/app/__testHelpers__/stub.js b/app/__testHelpers__/stub.js new file mode 100644 index 00000000..6ea59246 --- /dev/null +++ b/app/__testHelpers__/stub.js @@ -0,0 +1,37 @@ +import sinon from 'sinon' + +const stubs = new Map() + +export const stub = (target, name, handler) => { + if (stubs.get(target)) { + throw new Error(`already stubbed: ${name}`) + } + const stubbedTarget = sinon.stub(target, name) + if (typeof handler === 'function') { + stubbedTarget.callsFake(handler) + } + else { + stubbedTarget.value(handler) + } + stubs.set(stubbedTarget, name) +} + +export const restore = (target, name) => { + if (!target[name] || !target[name].restore) { + throw new Error(`not stubbed: ${name}`) + } + target[name].restore() + stubs.delete(target) +} + +export const overrideStub = (target, name, handler) => { + restore(target, name) + stub(target, name, handler) +} + +export const restoreAll = () => { + stubs.forEach((name, target) => { + stubs.delete(target) + target.restore() + }) +} diff --git a/app/__tests__/analytics/getDeviceData.tests.js b/app/__tests__/analytics/getDeviceData.tests.js new file mode 100644 index 00000000..91168081 --- /dev/null +++ b/app/__tests__/analytics/getDeviceData.tests.js @@ -0,0 +1,5 @@ +import { getDeviceData } from '../../lib/analystics/getDeviceData' + +describe(getDeviceData.name, function () { + test.todo('is not impl') +}) diff --git a/app/__tests__/components/ActionButton.tests.js b/app/__tests__/components/ActionButton.tests.js new file mode 100644 index 00000000..3a55918b --- /dev/null +++ b/app/__tests__/components/ActionButton.tests.js @@ -0,0 +1,5 @@ +import { ActionButton } from '../../lib/components/ActionButton' + +describe(ActionButton.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/BackButton.tests.js b/app/__tests__/components/BackButton.tests.js new file mode 100644 index 00000000..760d34db --- /dev/null +++ b/app/__tests__/components/BackButton.tests.js @@ -0,0 +1,5 @@ +import { BackButton } from '../../lib/components/BackButton' + +describe(BackButton.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/CatchErrors.tests.js b/app/__tests__/components/CatchErrors.tests.js new file mode 100644 index 00000000..2832510c --- /dev/null +++ b/app/__tests__/components/CatchErrors.tests.js @@ -0,0 +1,5 @@ +import { CatchErrors } from '../../lib/components/CatchErrors' + +describe(CatchErrors.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/CharacterInput.tests.js b/app/__tests__/components/CharacterInput.tests.js new file mode 100644 index 00000000..40609b40 --- /dev/null +++ b/app/__tests__/components/CharacterInput.tests.js @@ -0,0 +1,5 @@ +import { CharacterInput } from '../../lib/components/CharacterInput' + +describe(CharacterInput.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/Checkbox.tests.js b/app/__tests__/components/Checkbox.tests.js new file mode 100644 index 00000000..505c24a6 --- /dev/null +++ b/app/__tests__/components/Checkbox.tests.js @@ -0,0 +1,5 @@ +import { Checkbox } from '../../lib/components/Checkbox' + +describe(Checkbox.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/Confirm.tests.js b/app/__tests__/components/Confirm.tests.js new file mode 100644 index 00000000..1199c1de --- /dev/null +++ b/app/__tests__/components/Confirm.tests.js @@ -0,0 +1,5 @@ +import { Confirm } from '../../lib/components/Confirm' + +describe(Confirm.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/Connection.tests.js b/app/__tests__/components/Connection.tests.js new file mode 100644 index 00000000..6b62c4c0 --- /dev/null +++ b/app/__tests__/components/Connection.tests.js @@ -0,0 +1,5 @@ +import { Connecting } from '../../lib/components/Connecting' + +describe(Connecting.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/ErrorMessage.tests.js b/app/__tests__/components/ErrorMessage.tests.js new file mode 100644 index 00000000..961da09d --- /dev/null +++ b/app/__tests__/components/ErrorMessage.tests.js @@ -0,0 +1,5 @@ +import { ErrorMessage } from '../../lib/components/ErrorMessage' + +describe(ErrorMessage.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/FadePanel.tests.js b/app/__tests__/components/FadePanel.tests.js new file mode 100644 index 00000000..921bb508 --- /dev/null +++ b/app/__tests__/components/FadePanel.tests.js @@ -0,0 +1,5 @@ +import { FadePanel } from '../../lib/components/FadePanel' + +describe(FadePanel.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/LeaButton.tests.js b/app/__tests__/components/LeaButton.tests.js new file mode 100644 index 00000000..9629eeef --- /dev/null +++ b/app/__tests__/components/LeaButton.tests.js @@ -0,0 +1,5 @@ +import { LeaButton } from '../../lib/components/LeaButton' + +describe(LeaButton.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/LeaButtonGroup.tests.js b/app/__tests__/components/LeaButtonGroup.tests.js new file mode 100644 index 00000000..66b0176e --- /dev/null +++ b/app/__tests__/components/LeaButtonGroup.tests.js @@ -0,0 +1,5 @@ +import { LeaButtonGroup } from '../../lib/components/LeaButtonGroup' + +describe(LeaButtonGroup.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/LeaText.tests.js b/app/__tests__/components/LeaText.tests.js new file mode 100644 index 00000000..48df3ce4 --- /dev/null +++ b/app/__tests__/components/LeaText.tests.js @@ -0,0 +1,5 @@ +import { LeaText } from '../../lib/components/LeaText' + +describe(LeaText.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/Loading.tests.js b/app/__tests__/components/Loading.tests.js new file mode 100644 index 00000000..be7f6809 --- /dev/null +++ b/app/__tests__/components/Loading.tests.js @@ -0,0 +1,5 @@ +import { Loading } from '../../lib/components/Loading' + +describe(Loading.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/MarkdownWithTts.tests.js b/app/__tests__/components/MarkdownWithTts.tests.js new file mode 100644 index 00000000..9b1fca58 --- /dev/null +++ b/app/__tests__/components/MarkdownWithTts.tests.js @@ -0,0 +1,5 @@ +// import { Markdown } from '../../lib/components/MarkdownWithTTS' + +describe('MarkdownWithTTS', function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/NullComponent.tests.js b/app/__tests__/components/NullComponent.tests.js new file mode 100644 index 00000000..eff8e6f6 --- /dev/null +++ b/app/__tests__/components/NullComponent.tests.js @@ -0,0 +1,5 @@ +import { NullComponent } from '../../lib/components/NullComponent' + +describe(NullComponent.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/ProfileButton.tests.js b/app/__tests__/components/ProfileButton.tests.js new file mode 100644 index 00000000..7f3d9889 --- /dev/null +++ b/app/__tests__/components/ProfileButton.tests.js @@ -0,0 +1,5 @@ +import { ProfileButton } from '../../lib/components/ProfileButton' + +describe(ProfileButton.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/RouteButton.tests.js b/app/__tests__/components/RouteButton.tests.js new file mode 100644 index 00000000..3eac6948 --- /dev/null +++ b/app/__tests__/components/RouteButton.tests.js @@ -0,0 +1,5 @@ +import { RouteButton } from '../../lib/components/RouteButton' + +describe(RouteButton.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/SoundIcon.tests.js b/app/__tests__/components/SoundIcon.tests.js new file mode 100644 index 00000000..84eddb17 --- /dev/null +++ b/app/__tests__/components/SoundIcon.tests.js @@ -0,0 +1,5 @@ +import { SoundIcon } from '../../lib/components/SoundIcon' + +describe(SoundIcon.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/ViewContainer.tests.js b/app/__tests__/components/ViewContainer.tests.js new file mode 100644 index 00000000..f41cefdd --- /dev/null +++ b/app/__tests__/components/ViewContainer.tests.js @@ -0,0 +1,5 @@ +import { ViewContainer } from '../../lib/components/ViewContainer' + +describe(ViewContainer.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/factories/UnitContentElementFactory.tests.js b/app/__tests__/components/factories/UnitContentElementFactory.tests.js new file mode 100644 index 00000000..b281f266 --- /dev/null +++ b/app/__tests__/components/factories/UnitContentElementFactory.tests.js @@ -0,0 +1,53 @@ +import React from 'react' +import { render } from '@testing-library/react-native' +import { UnitContentElementFactory, useContentElementFactory } from '../../../lib/components/factories/UnitContentElementFactory' +import { simpleRandom } from '../../../__testHelpers__/simpleRandom' +import { Text } from 'react-native' + +describe('UnitContentElementFactory', () => { + afterEach(() => UnitContentElementFactory.flush()) + + describe(UnitContentElementFactory.register.name, () => { + it('registers a new renderer', () => { + const options = { + type: simpleRandom(), + subtype: simpleRandom(), + component: simpleRandom() + } + const unknown = { + type: simpleRandom(), + subtype: simpleRandom() + } + UnitContentElementFactory.register(options) + expect(UnitContentElementFactory.isRegistered(options)).toEqual(true) + expect(UnitContentElementFactory.isRegistered(unknown)).toEqual(false) + UnitContentElementFactory.flush() + expect(UnitContentElementFactory.isRegistered(options)).toEqual(false) + }) + }) + describe(UnitContentElementFactory.Renderer.name, () => { + it('renders by given keys', () => { + const props = { testID: simpleRandom() } + const text = simpleRandom() + const Component = () => ({text}) + const type = simpleRandom() + const subtype = simpleRandom() + const options = { type, subtype, component: Component } + UnitContentElementFactory.register(options) + + const { Renderer } = useContentElementFactory() + const { getAllByText } = render() + const elements = getAllByText(text) + expect(elements.length).toEqual(1) + }) + it('renders a fallback by unknown keys', () => { + const type = simpleRandom() + const subtype = simpleRandom() + const text = `Fallback: ${type} / ${subtype}` + const { Renderer } = useContentElementFactory() + const { getAllByText } = render() + const elements = getAllByText(text) + expect(elements.length).toEqual(1) + }) + }) +}) diff --git a/app/__tests__/components/factories/createRoutableComponent.tests.js b/app/__tests__/components/factories/createRoutableComponent.tests.js new file mode 100644 index 00000000..265b7ca2 --- /dev/null +++ b/app/__tests__/components/factories/createRoutableComponent.tests.js @@ -0,0 +1,5 @@ +import { createRoutableComponent } from '../../../lib/components/factories/createRoutableComponent' + +describe(createRoutableComponent.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/images/LeaLogo.tests.js b/app/__tests__/components/images/LeaLogo.tests.js new file mode 100644 index 00000000..d64ba96c --- /dev/null +++ b/app/__tests__/components/images/LeaLogo.tests.js @@ -0,0 +1,5 @@ +import { LeaLogo } from '../../../lib/components/images/LeaLogo' + +describe(LeaLogo.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/layout/Fill.tests.js b/app/__tests__/components/layout/Fill.tests.js new file mode 100644 index 00000000..7a8bef86 --- /dev/null +++ b/app/__tests__/components/layout/Fill.tests.js @@ -0,0 +1,5 @@ +import { Fill } from '../../../lib/components/layout/Fill' + +describe(Fill.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/progress/CurrentProgress.js b/app/__tests__/components/progress/CurrentProgress.js new file mode 100644 index 00000000..a925dda3 --- /dev/null +++ b/app/__tests__/components/progress/CurrentProgress.js @@ -0,0 +1,5 @@ +import { CurrentProgress } from '../../../lib/components/progress/CurrentProgress' + +describe(CurrentProgress.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/progress/Diamond.tests.js b/app/__tests__/components/progress/Diamond.tests.js new file mode 100644 index 00000000..bff58938 --- /dev/null +++ b/app/__tests__/components/progress/Diamond.tests.js @@ -0,0 +1,5 @@ +import { Diamond } from '../../../lib/components/progress/Diamond' + +describe(Diamond.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/progress/StaticCircularProgress.tests.js b/app/__tests__/components/progress/StaticCircularProgress.tests.js new file mode 100644 index 00000000..2e7cafbe --- /dev/null +++ b/app/__tests__/components/progress/StaticCircularProgress.tests.js @@ -0,0 +1,5 @@ +import { StaticCircularProgress } from '../../../lib/components/progress/StaticCircularProgress' + +describe(StaticCircularProgress.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/progress/computeProgress.tests.js b/app/__tests__/components/progress/computeProgress.tests.js new file mode 100644 index 00000000..f2a91310 --- /dev/null +++ b/app/__tests__/components/progress/computeProgress.tests.js @@ -0,0 +1,28 @@ +import { computeProgress } from '../../../lib/components/progress/computeProgress' + +describe(computeProgress.name, () => { + it('always returns a valid progress', () => { + [ + { current: undefined, max: 9, expected: 0.1 }, + { current: undefined, max: undefined, expected: 0.5 }, + { current: 5, max: undefined, expected: 1 }, + { current: -4, max: 9, expected: 0 }, + { current: -1, max: 9, expected: 0 }, + { current: 0, max: 9, expected: 0.1 }, + { current: 1, max: 9, expected: 0.2 }, + { current: 2, max: 9, expected: 0.3 }, + { current: 5, max: 9, expected: 0.6 }, + { current: 8, max: 9, expected: 0.9 }, + { current: 9, max: 9, expected: 1 }, + { current: 10, max: 9, expected: 1 }, + { current: 1, max: -1, expected: 1 }, + { current: 1, max: -2, expected: 0 }, + { current: 1, max: Infinity, expected: 0 }, + { current: Infinity, max: Infinity, expected: 0 }, + { current: {}, max: {}, expected: 0 }, + { current: 1, max: null, expected: 1 } + ].forEach(({ current, max, expected }) => { + expect(computeProgress({ current, max })).toEqual(expected) + }) + }) +}) diff --git a/app/__tests__/components/progress/correctDiamondProgress.tests.js b/app/__tests__/components/progress/correctDiamondProgress.tests.js new file mode 100644 index 00000000..4ee09d5d --- /dev/null +++ b/app/__tests__/components/progress/correctDiamondProgress.tests.js @@ -0,0 +1,34 @@ +import { correctDiamondProgress } from '../../../lib/components/progress/correctDiamondProgress' + +describe(correctDiamondProgress.name, () => { + it('falls back to 0 if no valid number is given', () => { + ['', '1', null, undefined, NaN, Infinity, {}, [], true, false, () => {}] + .forEach(value => { + expect(correctDiamondProgress(value)).toEqual(0) + }) + }) + it('returns the appropriate value for a given number', () => { + [ + { value: -2, expected: 0 }, + { value: -1, expected: 0 }, + { value: -0.1, expected: 0 }, + { value: 0, expected: 0 }, + { value: 0.01, expected: 0.3 }, + { value: 0.1, expected: 0.3 }, + { value: 0.24, expected: 0.3 }, + { value: 0.25, expected: 0.3 }, + { value: 0.31, expected: 0.31 }, + { value: 0.5, expected: 0.5 }, + { value: 0.74, expected: 0.74 }, + { value: 0.75, expected: 0.75 }, + { value: 0.76, expected: 0.75 }, + { value: 0.89, expected: 0.75 }, + { value: 0.9, expected: 1 }, + { value: 0.99, expected: 1 }, + { value: 1, expected: 1 }, + { value: 1.01, expected: 1 } + ].forEach(({ value, expected }) => { + expect(correctDiamondProgress(value)).toEqual(expected) + }) + }) +}) diff --git a/app/__tests__/components/renderer/media/ImageRenderer.tests.js b/app/__tests__/components/renderer/media/ImageRenderer.tests.js new file mode 100644 index 00000000..7864c8fb --- /dev/null +++ b/app/__tests__/components/renderer/media/ImageRenderer.tests.js @@ -0,0 +1,5 @@ +import { ImageRenderer } from '../../../../lib/components/renderer/media/ImageRenderer' + +describe(ImageRenderer.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/renderer/text/MarkdownRenderer.tests.js b/app/__tests__/components/renderer/text/MarkdownRenderer.tests.js new file mode 100644 index 00000000..0c67683e --- /dev/null +++ b/app/__tests__/components/renderer/text/MarkdownRenderer.tests.js @@ -0,0 +1,5 @@ +import { MarkdownRenderer } from '../../../../lib/components/renderer/text/Markdown' + +describe(MarkdownRenderer.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/renderer/text/PlainTextRenderer.tests.js b/app/__tests__/components/renderer/text/PlainTextRenderer.tests.js new file mode 100644 index 00000000..48d8a543 --- /dev/null +++ b/app/__tests__/components/renderer/text/PlainTextRenderer.tests.js @@ -0,0 +1,5 @@ +import { PlainTextRenderer } from '../../../../lib/components/renderer/text/PlainTextRenderer' + +describe(PlainTextRenderer.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/components/tts.test.js b/app/__tests__/components/tts.test.js new file mode 100644 index 00000000..8f58f57c --- /dev/null +++ b/app/__tests__/components/tts.test.js @@ -0,0 +1,359 @@ +import React from 'react' +import { fireEvent, render, act } from '@testing-library/react-native' +import { TTSengine, useTts } from '../../lib/components/Tts' +import { Colors } from '../../lib/constants/Colors' + +const setSpeechOptions = { timeout: 25 } + +it('speaks a given text', async () => { + let speakCalled = false + let stopCalled = false + + await TTSengine.setSpeech({ + isSpeakingAsync: async function () { + return false + }, + speak: (t) => { + expect(t).toBe('ttsMock.text') + speakCalled = true + }, + stop: () => { + stopCalled = true + } + }, setSpeechOptions) + + const { Tts } = useTts() + const { getByTestId } = render( + <> + + + ) + const ttsBtn = getByTestId('ttsMock.text') + await act(() => fireEvent.press(ttsBtn)) + + expect(speakCalled).toBe(true) + expect(stopCalled).toBe(false) +}) + +it('updates global TTSEngine props as side effect', async function () { + TTSengine.setSpeech({ + isSpeakingAsync: async function () { + return false + }, + speak: async (t, options) => { + if (TTSengine.isSpeaking) { await act(() => options.onDone()) } + else { await act(() => options.onStart()) } + }, + stop: () => {} + }, setSpeechOptions) + + const { Tts } = useTts() + const { getByTestId } = render( + <> + + + ) + const ttsBtn = getByTestId('ttsMock.text') + await act(() => fireEvent.press(ttsBtn)) + expect(TTSengine.isSpeaking).toBe(true) + expect(TTSengine.speakId).toBe('ttsMock.text') + expect(TTSengine.iconColor).toBe(Colors.primary) + + await act(() => fireEvent.press(ttsBtn)) +}) + +it('stops if the action is executed before the tts is done', async function () { + let speakCalled = false + let stopCalled = false + let isSpeakingCalled = false + await TTSengine.setSpeech({ + isSpeakingAsync: async function () { + if (isSpeakingCalled) { + return false + } + + isSpeakingCalled = true + return true + }, + speak: () => { + speakCalled = true + }, + stop: () => { + stopCalled = true + } + }, setSpeechOptions) + + const { Tts } = useTts() + const { getByTestId } = render( + <> + + + ) + const ttsBtn = getByTestId('ttsMock.text') + await act(() => fireEvent.press(ttsBtn)) + + expect(speakCalled).toBe(true) + expect(stopCalled).toBe(true) + expect(TTSengine.isSpeaking).toBe(false) +}) + +it('resolve to a complete state via options.onStart', async () => { + let speakCalled = false + let stopCalled = false + let opts + + await TTSengine.setSpeech({ + isSpeakingAsync: async function () { + return false + }, + speak: async (t, options) => { + opts = options + expect(t).toBe('ttsMock.text') + speakCalled = true + + await act(() => options.onStart()) + }, + stop: () => { + stopCalled = true + } + }, setSpeechOptions) + + const { Tts } = useTts() + const { getByTestId } = render( + <> + + + ) + const ttsBtn = getByTestId('ttsMock.text') + await act(() => fireEvent.press(ttsBtn)) + + expect(speakCalled).toBe(true) + expect(stopCalled).toBe(false) + + expect(TTSengine.isSpeaking).toBe(true) + expect(TTSengine.speakId).toBe('ttsMock.text') + expect(TTSengine.iconColor).toBe(Colors.primary) + + // clean up + await act(() => opts.onDone()) +}) + +it('resolve to a stopped state via options.onStopped', async () => { + let speakCalled = false + let stopCalled = false + + await TTSengine.setSpeech({ + isSpeakingAsync: async function () { + return false + }, + speak: async (t, options) => { + expect(t).toBe('ttsMock.text') + speakCalled = true + + await act(() => options.onStopped()) + }, + stop: () => { + stopCalled = true + } + }, setSpeechOptions) + + const { Tts } = useTts() + const { getByTestId } = render( + <> + + + ) + const ttsBtn = getByTestId('ttsMock.text') + await act(() => fireEvent.press(ttsBtn)) + await act(() => TTSengine.stop()) + + expect(speakCalled).toBe(true) + expect(stopCalled).toBe(true) + expect(TTSengine.isSpeaking).toBe(false) + expect(TTSengine.speakId).toBe(0) + expect(TTSengine.iconColor).toBe(Colors.primary) +}) + +it('allows to attach beforeSpeak listener', async () => { + let handlerRun = false + let opts + const beforeSpeakHandler = () => { + handlerRun = true + } + + TTSengine.on('beforeSpeak', beforeSpeakHandler) + + let speakCalled = false + let stopCalled = false + + await TTSengine.setSpeech({ + isSpeakingAsync: async function () { + return false + }, + speak: async (t, options) => { + opts = options + expect(t).toBe('ttsMock.text') + speakCalled = true + + await act(() => options.onStart()) + }, + stop: () => { + stopCalled = true + } + }, setSpeechOptions) + + const { Tts } = useTts() + const { getByTestId } = render( + <> + + + ) + const ttsBtn = getByTestId('ttsMock.text') + await act(() => fireEvent.press(ttsBtn)) + + expect(handlerRun).toBe(true) + expect(speakCalled).toBe(true) + expect(stopCalled).toBe(false) + expect(TTSengine.off('beforeSpeak', () => {})).toBe(false) + expect(TTSengine.off('beforeSpeak', beforeSpeakHandler)).toBe(true) + + // clean up + await act(() => opts.onDone()) +}) + +it('resolve to a complete state via options.onDone', async () => { + let speakCalled = false + let stopCalled = false + + await TTSengine.setSpeech({ + isSpeakingAsync: async function () { + return false + }, + speak: async (t, options) => { + expect(t).toBe('ttsMock.text') + speakCalled = true + + await act(() => options.onDone()) + }, + stop: () => { + stopCalled = true + } + }, setSpeechOptions) + + const { Tts } = useTts() + const { getByTestId } = render( + <> + + + ) + const ttsBtn = getByTestId('ttsMock.text') + await act(() => fireEvent.press(ttsBtn)) + + expect(speakCalled).toBe(true) + expect(stopCalled).toBe(true) + expect(TTSengine.isSpeaking).toBe(false) + expect(TTSengine.speakId).toBe(0) + expect(TTSengine.iconColor).toBe(Colors.primary) +}) + +it('start 2 different tts processes successively', async () => { + let stopCalled = false + let tts1Speaking = false + let opts + await TTSengine.setSpeech({ + isSpeakingAsync: async function () { + if (tts1Speaking) { + tts1Speaking = false + return true + } + return false + }, + speak: async (t, options) => { + if (opts) { + // simulate stop / override + await act(() => opts.onDone()) + } + opts = options + await act(() => options.onStart()) + }, + stop: () => { + stopCalled = true + } + }, setSpeechOptions) + + const { Tts } = useTts() + const render1 = render( + <> + + + ) + + const render2 = render( + <> + + + ) + + const ttsBtn = render1.getByTestId('ttsMock.text1') + const ttsBtn2 = render2.getByTestId('ttsMock.text2') + + await act(() => fireEvent.press(ttsBtn)) + expect(TTSengine.isSpeaking).toBe(true) + expect(TTSengine.speakId).toBe('ttsMock.text1') + expect(TTSengine.iconColor).toBe(Colors.primary) + expect(stopCalled).toBe(false) + tts1Speaking = true + // clean up + + await act(() => fireEvent.press(ttsBtn2)) + + expect(TTSengine.isSpeaking).toBe(true) + expect(TTSengine.speakId).toBe('ttsMock.text2') + expect(TTSengine.iconColor).toBe(Colors.primary) + expect(stopCalled).toBe(true) + + // clean up + await act(() => opts.onDone()) +}) + +describe('API', function () { + describe(TTSengine.component.name, function () { + it('returns the React component', () => { + const c = TTSengine.component() + expect(typeof c === 'function').toBe(true) + }) + }) + describe(TTSengine.updateSpeed.name, function () { + it('throws if speed is out of supported range', () => { + [-1, -0.1, 0, 0.09, 2.1, 3].forEach(value => { + expect(() => TTSengine.updateSpeed(value)) + .toThrow(`New speed not in range, expected ${value} between 0.1 and 2.0`) + }) + }) + it('sets the new speed', () => { + for (let i = 0.1; i <= 2.0; i += 0.1) { + TTSengine.updateSpeed(i) + expect(TTSengine.currentSpeed).toBe(i) + } + TTSengine.currentSpeed = 1 + }) + }) + describe(TTSengine.getVoices.name, function () { + it('returns voices, if they already exist', async () => { + TTSengine.availableVoices = [{ foo: 'bar' }] + const voices = await TTSengine.getVoices() + expect(voices).toEqual([{ foo: 'bar' }]) + }) + it('loads voices if not yet loaded', async () => { + TTSengine.availableVoices = null + const speechProvider = { + async getAvailableVoicesAsync () { + return [{ language: 'en-GB' }, { language: 'de-DE' }] + } + } + await TTSengine.setSpeech(speechProvider) + const voices = await TTSengine.getVoices() + expect(voices).toEqual([{ language: 'de-DE' }]) + }) + }) +}) diff --git a/app/__tests__/contexts/Achievements.tests.js b/app/__tests__/contexts/Achievements.tests.js new file mode 100644 index 00000000..e97187a6 --- /dev/null +++ b/app/__tests__/contexts/Achievements.tests.js @@ -0,0 +1,6 @@ +import { Achievements } from '../../lib/contexts/Achievements' +import { createContextBaseTests } from '../../__testHelpers__/createContextBaseTests' + +describe(Achievements.name, function () { + createContextBaseTests({ ctx: Achievements }) +}) diff --git a/app/__tests__/contexts/Dimension.tests.js b/app/__tests__/contexts/Dimension.tests.js new file mode 100644 index 00000000..f1bc50ab --- /dev/null +++ b/app/__tests__/contexts/Dimension.tests.js @@ -0,0 +1,6 @@ +import { Dimension } from '../../lib/contexts/Dimension' +import { createContextBaseTests } from '../../__testHelpers__/createContextBaseTests' + +describe(Dimension.name, function () { + createContextBaseTests({ ctx: Dimension }) +}) diff --git a/app/__tests__/contexts/Feedback.tests.js b/app/__tests__/contexts/Feedback.tests.js new file mode 100644 index 00000000..03111961 --- /dev/null +++ b/app/__tests__/contexts/Feedback.tests.js @@ -0,0 +1,6 @@ +import { Feedback } from '../../lib/contexts/Feedback' +import { createContextBaseTests } from '../../__testHelpers__/createContextBaseTests' + +describe(Feedback.name, function () { + createContextBaseTests({ ctx: Feedback }) +}) diff --git a/app/__tests__/contexts/Field.tests.js b/app/__tests__/contexts/Field.tests.js new file mode 100644 index 00000000..2dfc4e52 --- /dev/null +++ b/app/__tests__/contexts/Field.tests.js @@ -0,0 +1,6 @@ +import { Field } from '../../lib/contexts/Field' +import { createContextBaseTests } from '../../__testHelpers__/createContextBaseTests' + +describe(Field.name, function () { + createContextBaseTests({ ctx: Field }) +}) diff --git a/app/__tests__/contexts/Legal.tests.js b/app/__tests__/contexts/Legal.tests.js new file mode 100644 index 00000000..c08ba960 --- /dev/null +++ b/app/__tests__/contexts/Legal.tests.js @@ -0,0 +1,6 @@ +import { Legal } from '../../lib/contexts/Legal' +import { createContextBaseTests } from '../../__testHelpers__/createContextBaseTests' + +describe(Legal.name, function () { + createContextBaseTests({ ctx: Legal }) +}) diff --git a/app/__tests__/contexts/Level.tests.js b/app/__tests__/contexts/Level.tests.js new file mode 100644 index 00000000..d25c27a6 --- /dev/null +++ b/app/__tests__/contexts/Level.tests.js @@ -0,0 +1,6 @@ +import { Level } from '../../lib/contexts/Level' +import { createContextBaseTests } from '../../__testHelpers__/createContextBaseTests' + +describe(Level.name, function () { + createContextBaseTests({ ctx: Level }) +}) diff --git a/app/__tests__/contexts/MapIcons.tests.js b/app/__tests__/contexts/MapIcons.tests.js new file mode 100644 index 00000000..9d35f410 --- /dev/null +++ b/app/__tests__/contexts/MapIcons.tests.js @@ -0,0 +1,62 @@ +import { MapIcons } from '../../lib/contexts/MapIcons' +import { createContextBaseTests } from '../../__testHelpers__/createContextBaseTests' +import { simpleRandomHex } from '../../lib/utils/simpleRandomHex' +import { render } from '@testing-library/react-native'; + +describe(MapIcons.name, () => { + createContextBaseTests({ ctx: MapIcons }) + + describe(MapIcons.getIncrementalIconIndex.name, () => { + it('always returns -1 if no icons are registered', () => { + MapIcons.setField(simpleRandomHex()) + expect(MapIcons.getIncrementalIconIndex()).toBe(-1) + }) + it('returns a count that rotates around the icons set size', async () => { + const fieldId = simpleRandomHex() + await MapIcons.collection().insert({ + fieldId, + icons: ['foo', 'bar', 'baz'] + }) + + MapIcons.setField(fieldId) + + const size = MapIcons.size() + expect(size).toBe(3) + + let prev = -1 + for (let i = 0; i < size; i++) { + const count = MapIcons.getIncrementalIconIndex() + expect(count).toBeGreaterThan(prev) + prev = count + } + // expect reset + expect(MapIcons.getIncrementalIconIndex()).toBe(0) + }) + }) + describe(MapIcons.render.name, () => { + it('returns null if the index is not within bounds', async () => { + const fieldId = simpleRandomHex() + const icons = ['edit', 'check', 'times'] + await MapIcons.collection().insert({ fieldId, icons }) + ;[-3, -2, -1, 3, 4, 5].forEach(index => { + expect(MapIcons.render(index)) + .toBe(null) + }) + }) + it('renders an icon by given index', async () => { + const fieldId = simpleRandomHex() + const icons = ['edit', 'check', 'times'] + await MapIcons.collection().insert({ fieldId, icons }) + + MapIcons.setField(fieldId) + const size = MapIcons.size() + + for (let i = 0; i < size; i++) { + const MapIcon = MapIcons.render(i) + const component = render(MapIcon) + const tree = component.toJSON() + expect(tree).toMatchSnapshot() + } + }) + }) +}) diff --git a/app/__tests__/contexts/Order.tests.js b/app/__tests__/contexts/Order.tests.js new file mode 100644 index 00000000..c1939b71 --- /dev/null +++ b/app/__tests__/contexts/Order.tests.js @@ -0,0 +1,6 @@ +import { Order } from '../../lib/contexts/Order' +import { createContextBaseTests } from '../../__testHelpers__/createContextBaseTests' + +describe(Order.name, function () { + createContextBaseTests({ ctx: Order }) +}) diff --git a/app/__tests__/contexts/Sync.tests.js b/app/__tests__/contexts/Sync.tests.js new file mode 100644 index 00000000..a359e515 --- /dev/null +++ b/app/__tests__/contexts/Sync.tests.js @@ -0,0 +1,228 @@ +import Meteor from '@meteorrn/core' +import { Sync } from '../../lib/infrastructure/sync/Sync' +import { simpleRandom } from '../../__testHelpers__/simpleRandom' +import { collectionExists } from '../../lib/infrastructure/collections/collections' +import { restoreAll, stub, overrideStub } from '../../__testHelpers__/stub' +import { createContextStorage } from '../../lib/contexts/createContextStorage' +import { cleanup } from '@testing-library/react-native' +import { ContextRepository } from '../../lib/infrastructure/ContextRepository' +import { createCollection } from '../../lib/infrastructure/createCollection' +import { mockCall } from '../../__testHelpers__/mockCall' +const AsyncStorage = require('../../__mocks__/@react-native-async-storage/async-storage') + +describe(`${Sync.name}-system`, function () { + beforeAll(() => { + expect(() => Sync.collection()) + .toThrow(`Collection ${Sync.name} not initialized`) + if (!collectionExists(Sync.name)) { + const collection = createCollection({ + name: Sync.name, + isLocal: true + }) + Sync.collection = () => collection + } + }) + + afterEach(() => { + restoreAll() + cleanup() + }) + + describe(Sync.init.name, function () { + it('throws on other methods if Sync is not initialized', async () => { + const expected = 'Sync.init must be called first' + await expect(Sync.isRequired()).rejects.toThrow(expected) + const onProgress = () => {} + await expect(Sync.run({ onProgress })).rejects.toThrow(expected) + }) + it('ensures there is a Sync doc', async () => { + expect(Sync.collection().findOne()).toBe(undefined) + await AsyncStorage.getItem.mockReturnValueOnce(null) + await Sync.init() + const { _id, _version, ...doc } = Sync.collection().findOne() + expect(_id).toBeTruthy() + expect(doc).toEqual({}) + Sync.collection().remove({}) + }) + it('loads the latest local sync doc from storage', async () => { + const doc = { _id: simpleRandom(), _version: 1, foo: 'bar' } + await AsyncStorage.getItem.mockReturnValueOnce(Meteor.EJSON.stringify(doc)) + await Sync.init() + expect(Sync.collection().findOne()).toEqual(doc) + }) + }) + + describe(Sync.isRequired.name, function () { + afterEach(() => { + Sync.reset() + }) + it('returns false if no context is required to be synced', async () => { + const data = {} + mockCall((name, doc, callback) => callback(null, data)) + + const isRequired = await Sync.isRequired() + expect(Sync.getQueue()).toEqual([]) + expect(isRequired).toBe(false) + }) + it('returns true if a context is required to be synced', async () => { + const data = { + dimension: { + updatedAt: new Date(), + hash: simpleRandom() + }, + foo: { + updatedAt: new Date(), + hash: simpleRandom() + } + } + mockCall((name, doc, callback) => callback(null, data)) + stub(Sync.collection(), 'findOne', () => ({ + dimension: { + updatedAt: new Date(), + hash: simpleRandom() // outdated + }, + foo: { + updatedAt: data.foo.updatedAt, + hash: data.foo.hash + } + })) + const isRequired = await Sync.isRequired() + expect(Sync.getQueue()).toEqual([{ + key: 'dimension', + hash: data.dimension.hash, + updatedAt: data.dimension.updatedAt + }]) + expect(isRequired).toBe(true) + }) + }) + + describe(Sync.syncContext.name, () => { + let name + let collection + let storage + + beforeAll(() => { + name = simpleRandom() + collection = createCollection({ name, isLocal: true }) + storage = createContextStorage({ name }) + }) + + it('skips sync if no docs were returned from server', async () => { + mockCall((name, doc, callback) => callback(null, null)) + + expect(await Sync.syncContext({ name, collection, storage })) + .toBe(false) + }) + + it('inserts received docs from server', async () => { + const docs = [ + { _id: simpleRandom(), foo: 'bar' }, + { _id: simpleRandom(), bar: 'moo' } + ] + + mockCall((name, doc, callback) => callback(null, docs)) + stub(storage, 'saveFromCollection', () => {}) + + // existing docs are only updated + await collection.insert({ ...docs[0], foo: 'moo' }) + await collection.insert({ _id: simpleRandom(), moo: 'milk' }) + + const synced = await Sync.syncContext({ name, collection, storage }) + expect(synced).toBe(true) + + expect(collection.find().count()).toBe(2) + expect(collection.findOne(docs[0]._id)).toStrictEqual({ + ...docs[0], + _version: 2 // updated + }) + expect(collection.findOne(docs[1]._id)).toStrictEqual({ + ...docs[1], + _version: 1 // inserted + }) + }) + }) + + describe(Sync.run.name, function () { + it('throws if there is no sync required', async () => { + Sync.reset() + const onProgress = () => {} + await expect(Sync.run({ onProgress })).rejects.toThrow('Sync should not run if not required') + }) + it('syncs the context and dispatches progress', async () => { + const name = simpleRandom() + const storage = createContextStorage({ name }) + const targetCollection = createCollection({ name, isLocal: true }) + + Sync.collection().remove({}) + Sync.collection().insert({}) + + const data = { + [name]: { + updatedAt: new Date(), + hash: simpleRandom() + } + } + + mockCall((name, doc, callback) => callback(null, data)) + + const isRequired = await Sync.isRequired() + expect(Sync.getQueue()).toEqual([{ + key: name, + hash: data[name].hash, + updatedAt: data[name].updatedAt + }]) + + expect(isRequired).toBe(true) + + // run actual sync + const newDocs = [ + { _id: simpleRandom(), _version: 1, foo: 'bar' }, + { _id: simpleRandom(), _version: 1, bar: 'moo' } + ] + + stub(storage, 'saveFromCollection', () => {}) + + // let backend return dimension docs + mockCall((name, doc, callback) => callback(null, newDocs)) + let progress = 0 + + const onProgress = (data) => { + progress = data.progress + } + + // should throw if no ctx found + await expect(() => Sync.run({ onProgress })) + .rejects.toThrow(`Expected ctx for key ${name}`) + + // make ctx to be found + ContextRepository.add(name, { name, collection: () => targetCollection, storage }) + + const updateSync = await Sync.run({ onProgress }) + expect(updateSync).toEqual({ + [name]: { + hash: data[name].hash, + updatedAt: data[name].updatedAt + } + }) + + expect(await Sync.isRequired()).toBe(false) + expect(progress).toEqual(1) + expect(Sync.getQueue()).toEqual([]) + + // docs in collection + expect(targetCollection.find().count()).toBe(newDocs.length) + newDocs.forEach(doc => { + expect(targetCollection.findOne(doc._id)).toEqual(doc) + }) + + // sync updated + const { _id, _version, ...updatedSyncDoc } = Sync.collection().findOne() + expect(updatedSyncDoc).toEqual({ + [name]: { + hash: data[name].hash, + updatedAt: data[name].updatedAt + } + }) + }) + }) +}) diff --git a/app/__tests__/contexts/UserProgress.tests.js b/app/__tests__/contexts/UserProgress.tests.js new file mode 100644 index 00000000..78cd5ddc --- /dev/null +++ b/app/__tests__/contexts/UserProgress.tests.js @@ -0,0 +1,197 @@ +import { UserProgress } from '../../lib/contexts/UserProgress' +import { createContextBaseTests } from '../../__testHelpers__/createContextBaseTests' +import { simpleRandomHex } from '../../lib/utils/simpleRandomHex' +import { getCollection } from '../../lib/infrastructure/collections/collections' +import Meteor from '@meteorrn/core' +import { mockCall } from '../../__testHelpers__/mockCall' + +describe(UserProgress.name, function () { + createContextBaseTests({ ctx: UserProgress }) + + describe(UserProgress.update.name, () => { + it('skips if no progress doc is available', async () => { + const fieldId = simpleRandomHex() + const updated = await UserProgress.update({ fieldId }) + expect(updated).toBe(false) + expect(getCollection(UserProgress.name) + .findOne({ fieldId })) + .toBe(undefined) + }) + it('updates the progress doc', async () => { + const ProgressCollection = UserProgress.collection() + const fieldId = simpleRandomHex() + const unitSetDoc = { + _id: simpleRandomHex(), + dimensionId: simpleRandomHex(), + progress: 0, + competencies: 0 + } + const userId = simpleRandomHex() + const docId = await ProgressCollection.insert({ userId, fieldId }) + const beforeDoc = ProgressCollection.findOne(docId) + const updated = await UserProgress.update({ fieldId, unitSetDoc }) + expect(updated).toBe(true) + + const updatedDoc = ProgressCollection.findOne(docId) + expect(updatedDoc).not.toStrictEqual(beforeDoc) + expect(updatedDoc).toStrictEqual({ + _id: docId, + fieldId, + _version: 2, + userId, + [unitSetDoc._id]: unitSetDoc + }) + }) + }) + describe(UserProgress.fetchFromServer.name, function () { + it('returns undefined if no doc was loaded from server', async () => { + mockCall((name, args, callback) => { + callback(null, null) + }) + const fieldId = simpleRandomHex() + const fetched = await UserProgress.fetchFromServer({ fieldId }) + expect(fetched).toBe(undefined) + }) + it('fetches a progress doc from the server and creates a new progress doc if not defined', async () => { + const dimensionId = simpleRandomHex() + const fieldId = simpleRandomHex() + const unitSet1 = { + _id: simpleRandomHex(), + dimensionId, + progress: 3, + competencies: 50, + complete: true + } + const unitSet2 = { + _id: simpleRandomHex(), + dimensionId, + progress: 16, + competencies: 207, + complete: false + } + const serverDoc = { + _id: simpleRandomHex(), + userId: simpleRandomHex(), + fieldId, + unitSets: [unitSet1, unitSet2] + } + jest + .spyOn(Meteor, 'call') + .mockImplementation((name, args, callback) => { + callback(null, serverDoc) + }) + + jest + .spyOn(Meteor, 'status') + .mockImplementation(() => { + return { + status: 'connected', + connected: true + } + }) + + const fetched = await UserProgress.fetchFromServer({ fieldId }) + expect(fetched).toStrictEqual({ + _id: serverDoc._id, + fieldId: serverDoc.fieldId, + userId: serverDoc.userId, + _version: 1, + unitSets: { + [unitSet1._id]: unitSet1, + [unitSet2._id]: unitSet2 + } + }) + }) + it('fetches a progress doc from the server and updates an existing progress doc if defined', async () => { + const dimensionId = simpleRandomHex() + const fieldId = simpleRandomHex() + const unitSet1 = { + _id: simpleRandomHex(), + dimensionId, + progress: 3, + competencies: 50, + complete: true + } + const unitSet2 = { + _id: simpleRandomHex(), + dimensionId, + progress: 16, + competencies: 207, + complete: false + } + const serverDoc = { + _id: simpleRandomHex(), + userId: simpleRandomHex(), + fieldId, + unitSets: [unitSet1, unitSet2] + } + jest + .spyOn(Meteor, 'call') + .mockImplementation((name, args, callback) => { + callback(null, { ...serverDoc }) + }) + + jest + .spyOn(Meteor, 'status') + .mockImplementation(() => { + return { + status: 'connected', + connected: true + } + }) + + await UserProgress.collection().insert({ + _id: serverDoc._id, + fieldId: serverDoc.fieldId, + userId: serverDoc.userId, + unitSets: {} + }) + const fetched = await UserProgress.fetchFromServer({ fieldId }) + expect(fetched).toStrictEqual({ + _id: serverDoc._id, + fieldId: serverDoc.fieldId, + userId: serverDoc.userId, + _version: 2, + unitSets: { + [unitSet1._id]: unitSet1, + [unitSet2._id]: unitSet2 + } + }) + }) + }) + describe(UserProgress.get.name, function () { + it('skips if no fieldId is given', async () => { + jest + .spyOn(UserProgress, 'fetchFromServer') + .mockImplementation(() => { + throw new Error('Unexpected call') + }) + const args = [undefined, {}, { fieldId: undefined }, { fieldId: null }] + for (const options of args) { + const doc = await UserProgress.get(options) + expect(doc).toBe(undefined) + } + }) + it('returns a document if it locally exists', async () => { + jest + .spyOn(UserProgress, 'fetchFromServer') + .mockImplementation(() => { + throw new Error('Unexpected call') + }) + const fieldId = simpleRandomHex() + const docId = await UserProgress.collection().insert({ fieldId }) + const expectedDoc = UserProgress.collection().findOne(docId) + const receivedDoc = await UserProgress.get({ fieldId }) + expect(receivedDoc).toStrictEqual(expectedDoc) + }) + it('fetches from server if doc does not exist', async () => { + const fieldId = simpleRandomHex() + const serverDoc = { _id: simpleRandomHex(), fieldId } + jest + .spyOn(UserProgress, 'fetchFromServer') + .mockImplementation(async () => serverDoc) + const receivedDoc = await UserProgress.get({ fieldId }) + expect(receivedDoc).toStrictEqual(serverDoc) + }) + }) +}) diff --git a/app/__tests__/contexts/__snapshots__/MapIcons.tests.js.snap b/app/__tests__/contexts/__snapshots__/MapIcons.tests.js.snap new file mode 100644 index 00000000..3e0adbb3 --- /dev/null +++ b/app/__tests__/contexts/__snapshots__/MapIcons.tests.js.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`mapIcons renders an icon by given index 1`] = ` + + + +`; + +exports[`mapIcons renders an icon by given index 2`] = ` + + + +`; + +exports[`mapIcons renders an icon by given index 3`] = ` + + + +`; diff --git a/app/__tests__/contexts/createContextStorage.tests.js b/app/__tests__/contexts/createContextStorage.tests.js new file mode 100644 index 00000000..456b215c --- /dev/null +++ b/app/__tests__/contexts/createContextStorage.tests.js @@ -0,0 +1,59 @@ +import { createContextStorage } from '../../lib/contexts/createContextStorage' +import { simpleRandom } from '../../__testHelpers__/simpleRandom' +import Meteor from '@meteorrn/core' +import { addCollection } from '../../lib/infrastructure/collections/collections' +const AsyncStorage = require('../../__mocks__/@react-native-async-storage/async-storage') + +const createCtx = () => { + const name = simpleRandom() + const c = new Meteor.Collection(null) + addCollection(name, c) + const collection = () => c + return { name, collection } +} + +const createData = () => [ + { _id: simpleRandom(), _version: 1, foo: 'bar' }, + { _id: simpleRandom(), _version: 1, bar: 'moo' } +].sort() + +describe(createContextStorage.name, function () { + beforeAll(() => { + jest.useFakeTimers({ advanceTimers: true }) + }) + it('creates a new storage instance', () => { + const ctx = createCtx() + const storage = createContextStorage(ctx) + expect(storage.name).toBe(ctx.name) + }) + + it('loads data from storage into collection', async () => { + const data = createData() + AsyncStorage.getItem.mockReturnValueOnce(Meteor.EJSON.stringify(data)) + const ctx = createCtx() + const storage = createContextStorage(ctx) + await storage.loadIntoCollection() + const docs = ctx.collection().find().fetch() + expect(docs.length).toEqual(data.length) + + data.forEach(doc => { + expect(ctx.collection().findOne(doc._id)).toEqual(doc) + }) + }) + + it('saves data from collection to storage', async () => { + const data = createData() + const ctx = createCtx() + data.forEach(doc => ctx.collection().insert(doc)) + const storage = createContextStorage(ctx) + await storage.saveFromCollection() + + const loaded = await AsyncStorage.getItem(storage.key) + const docs = Meteor.EJSON.parse(loaded) + expect(docs.length).toEqual(data.length) + docs.forEach(doc => { + const found = data.find(element => element._id === doc._id) + expect(found).toEqual(doc) + }) + }) +}) diff --git a/app/__tests__/env/Config.tests.js b/app/__tests__/env/Config.tests.js new file mode 100644 index 00000000..d06191e8 --- /dev/null +++ b/app/__tests__/env/Config.tests.js @@ -0,0 +1,5 @@ +import { Config } from '../../lib/env/Config' + +describe(Config.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/env/Sound.tests.js b/app/__tests__/env/Sound.tests.js new file mode 100644 index 00000000..287a7deb --- /dev/null +++ b/app/__tests__/env/Sound.tests.js @@ -0,0 +1,5 @@ +import { Sound } from '../../lib/env/Sound' + +describe(Sound.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/env/loadSettingsFromUserProfile.tests.js b/app/__tests__/env/loadSettingsFromUserProfile.tests.js new file mode 100644 index 00000000..bc509995 --- /dev/null +++ b/app/__tests__/env/loadSettingsFromUserProfile.tests.js @@ -0,0 +1,5 @@ +import { loadSettingsFromUserProfile } from '../../lib/env/loadSettingsFromUserProfile' + +describe(loadSettingsFromUserProfile.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/errors/ErrorReporter.tests.js b/app/__tests__/errors/ErrorReporter.tests.js new file mode 100644 index 00000000..8421676c --- /dev/null +++ b/app/__tests__/errors/ErrorReporter.tests.js @@ -0,0 +1,8 @@ +import { ErrorReporter } from '../../lib/errors/ErrorReporter' +// import { MeteorError } from '../../lib/errors/MeteorError' +// mport { AuthenticationError } from '../../lib/errors/AuthenticationError' +// import { ConnectionError } from '../../lib/errors/ConnectionError' + +describe(ErrorReporter.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/errors/normalizeError.tests.js b/app/__tests__/errors/normalizeError.tests.js new file mode 100644 index 00000000..d338222d --- /dev/null +++ b/app/__tests__/errors/normalizeError.tests.js @@ -0,0 +1,8 @@ +import { normalizeError } from '../../lib/errors/normalizeError' +// import { MeteorError } from '../../lib/errors/MeteorError' +// import { AuthenticationError } from '../../lib/errors/AuthenticationError' +// import { ConnectionError } from '../../lib/errors/ConnectionError' + +describe(normalizeError.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/hooks/useBackHandler.tests.js b/app/__tests__/hooks/useBackHandler.tests.js new file mode 100644 index 00000000..6ca3cb68 --- /dev/null +++ b/app/__tests__/hooks/useBackHandler.tests.js @@ -0,0 +1,5 @@ +import { useBackHandler } from '../../lib/hooks/useBackHandler' + +describe(useBackHandler.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/hooks/useConnection.tests.js b/app/__tests__/hooks/useConnection.tests.js new file mode 100644 index 00000000..5383c92b --- /dev/null +++ b/app/__tests__/hooks/useConnection.tests.js @@ -0,0 +1,5 @@ +// import { useConnection } from '../../lib/hooks/useConnection' + +describe('useConnection', function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/hooks/useKeyboardVisibilityHandler.tests.js b/app/__tests__/hooks/useKeyboardVisibilityHandler.tests.js new file mode 100644 index 00000000..83eb3dc2 --- /dev/null +++ b/app/__tests__/hooks/useKeyboardVisibilityHandler.tests.js @@ -0,0 +1,5 @@ +import { useKeyboardVisibilityHandler } from '../../lib/hooks/useKeyboardVisibilityHandler' + +describe(useKeyboardVisibilityHandler.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/hooks/useLogin.tests.js b/app/__tests__/hooks/useLogin.tests.js new file mode 100644 index 00000000..ff396f89 --- /dev/null +++ b/app/__tests__/hooks/useLogin.tests.js @@ -0,0 +1,5 @@ +import { useLogin } from '../../lib/hooks/useLogin' + +describe(useLogin.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/hooks/useProgress.tests.js b/app/__tests__/hooks/useProgress.tests.js new file mode 100644 index 00000000..a07a926a --- /dev/null +++ b/app/__tests__/hooks/useProgress.tests.js @@ -0,0 +1,5 @@ +import { useProgress } from '../../lib/hooks/useProgress' + +describe(useProgress.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/hooks/useScreenIsActive.tests.js b/app/__tests__/hooks/useScreenIsActive.tests.js new file mode 100644 index 00000000..603ebaeb --- /dev/null +++ b/app/__tests__/hooks/useScreenIsActive.tests.js @@ -0,0 +1,5 @@ +import { useScreenIsActive } from '../../lib/hooks/screenIsActive' + +describe(useScreenIsActive.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/hooks/useTimeout.tests.js b/app/__tests__/hooks/useTimeout.tests.js new file mode 100644 index 00000000..a9d79354 --- /dev/null +++ b/app/__tests__/hooks/useTimeout.tests.js @@ -0,0 +1,5 @@ +import { useTimeout } from '../../lib/hooks/useTimeout' + +describe(useTimeout.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/hooks/useUser.tests.js b/app/__tests__/hooks/useUser.tests.js new file mode 100644 index 00000000..21351ae0 --- /dev/null +++ b/app/__tests__/hooks/useUser.tests.js @@ -0,0 +1,5 @@ +import { useUser } from '../../lib/hooks/useUser' + +describe(useUser.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/hooks/useVoices.tests.js b/app/__tests__/hooks/useVoices.tests.js new file mode 100644 index 00000000..091bf678 --- /dev/null +++ b/app/__tests__/hooks/useVoices.tests.js @@ -0,0 +1,5 @@ +import { useVoices } from '../../lib/hooks/useVoices' + +describe(useVoices.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/i18n/i18n.test.js b/app/__tests__/i18n/i18n.test.js new file mode 100644 index 00000000..c7c7b282 --- /dev/null +++ b/app/__tests__/i18n/i18n.test.js @@ -0,0 +1,28 @@ +import { i18n } from '../../lib/i18n' + +const translationEN = i18n.getDataByLanguage('en') +const translationDE = i18n.getDataByLanguage('de') + +test('recursively iterate all object keys of i18 EN and DE, checks if the same namespaces exists, if namespaces have the same length', () => { + const toKeys = (obj, keys = [], path = '') => { + Object.entries(obj).forEach(([key, value]) => { + const type = typeof value + if (value === null || (type !== 'object' && type !== 'string')) { + throw new Error(`Expected object|string, got ${value}`) + } + const newPath = `${path}.${key}` + if (type === 'string') { + keys.push(newPath) + } + else { + toKeys(value, keys, newPath) + } + }) + return keys + } + const byName = (a,b) => a.localeCompare(b) + const deKeys = toKeys(translationDE).sort(byName) + const enKeys = toKeys(translationEN).sort(byName) + + expect(deKeys).toEqual(enKeys) +}) diff --git a/app/__tests__/infrastructure/app/AppTemrinate.tests.js b/app/__tests__/infrastructure/app/AppTemrinate.tests.js new file mode 100644 index 00000000..428e91c7 --- /dev/null +++ b/app/__tests__/infrastructure/app/AppTemrinate.tests.js @@ -0,0 +1,5 @@ +import { AppTerminate } from '../../../lib/infrastructure/app/AppTerminate' + +describe(AppTerminate.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/infrastructure/collections/collection.tests.js b/app/__tests__/infrastructure/collections/collection.tests.js new file mode 100644 index 00000000..4b970bb1 --- /dev/null +++ b/app/__tests__/infrastructure/collections/collection.tests.js @@ -0,0 +1,5 @@ +// import { getCollection, addCollection } from '../../../lib/infrastructure/collections/collections' + +describe('collections', function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/infrastructure/createRepository.tests.js b/app/__tests__/infrastructure/createRepository.tests.js new file mode 100644 index 00000000..dbda7a9c --- /dev/null +++ b/app/__tests__/infrastructure/createRepository.tests.js @@ -0,0 +1,17 @@ +import { createRepository } from '../../lib/infrastructure/createRepository' +import { simpleRandom } from '../../__testHelpers__/simpleRandom' + +describe(createRepository.name, function () { + it('creates a repository-pattern impl', () => { + const repo = createRepository() + const name = simpleRandom() + const value = simpleRandom() + + expect(repo.has(name)).toEqual(false) + repo.add(name, value) + expect(repo.has(name)).toEqual(true) + expect(repo.get(name)).toEqual(value) + expect(() => repo.add(name, value)) + .toThrow(`Entry "${name}" already exists`) + }) +}) diff --git a/app/__tests__/infrastructure/factories/createCollection.tests.js b/app/__tests__/infrastructure/factories/createCollection.tests.js new file mode 100644 index 00000000..a55ef29d --- /dev/null +++ b/app/__tests__/infrastructure/factories/createCollection.tests.js @@ -0,0 +1,5 @@ +import { createCollection } from '../../../lib/infrastructure/createCollection' + +describe(createCollection.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/infrastructure/log/log.tests.js b/app/__tests__/infrastructure/log/log.tests.js new file mode 100644 index 00000000..ad48bf3d --- /dev/null +++ b/app/__tests__/infrastructure/log/log.tests.js @@ -0,0 +1,5 @@ +import { Log } from '../../../lib/infrastructure/Log' + +describe(Log.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/items/choice/Choice.tests.js b/app/__tests__/items/choice/Choice.tests.js new file mode 100644 index 00000000..62bdf01f --- /dev/null +++ b/app/__tests__/items/choice/Choice.tests.js @@ -0,0 +1,190 @@ +import { Choice } from '../../../lib/items/choice/Choice' +import { scoreChoice } from '../../../lib/items/choice/scoring' +import { simpleRandom } from '../../../__testHelpers__/simpleRandom' +import { Scoring } from '../../../lib/scoring/Scoring' +import { toInteger } from '../../../lib/utils/number/toInteger' +import { isUndefinedResponse } from '../../../lib/scoring/isUndefinedResponse' + +const createItemDoc = ({ flavor, competency, correctResponse, requires } = {}) => { + return { + flavor: flavor ?? Choice.flavors.single.value, + scoring: [{ + competency: competency ?? simpleRandom(), + correctResponse: correctResponse ?? Math.round(Math.random() * 100).toString(10), + requires: requires ?? simpleRandom() + }] + } +} + +describe(Choice.name, () => { + describe(scoreChoice.name, () => { + it('throws when there is an unknown flavor', () => { + [ + { scoring: [{}] }, + { scoring: [{}], flavor: -99 } + ].forEach(value => { + expect(() => scoreChoice(value)) + .toThrow(`Unexpected choice flavor: ${value.flavor}`) + }) + }) + + describe(Choice.flavors.single.name, () => { + it('scores an undefined single choice response', () => { + const itemDoc = createItemDoc() + + ;['', null, undefined, Scoring.UNDEFINED].forEach(value => { + const responseDoc = { responses: [value] } + expect(scoreChoice(itemDoc, responseDoc)).toEqual([{ + competency: itemDoc.scoring[0].competency, + correctResponse: itemDoc.scoring[0].correctResponse, + value, + score: false, + isUndefined: true + }]) + }) + }) + it('scores a truthy single choice response', () => { + const itemDoc = createItemDoc() + const responseDoc = { responses: [itemDoc.scoring[0].correctResponse] } + const result = scoreChoice(itemDoc, responseDoc) + expect(result).toEqual([{ + competency: itemDoc.scoring[0].competency, + correctResponse: itemDoc.scoring[0].correctResponse, + value: Number.parseInt(itemDoc.scoring[0].correctResponse), + score: true, + isUndefined: false + }]) + }) + it('scores a falsy single choice response', () => { + const itemDoc = createItemDoc() + const responseDoc = { responses: ['-99'] } + const result = scoreChoice(itemDoc, responseDoc) + expect(result).toEqual([{ + competency: itemDoc.scoring[0].competency, + correctResponse: itemDoc.scoring[0].correctResponse, + value: -99, + score: false, + isUndefined: false + }]) + }) + }) + + describe(Choice.flavors.multiple.name, () => { + it('scores undefined multiple choice response', () => { + const itemDoc = createItemDoc({ + flavor: Choice.flavors.multiple.value, + correctResponse: [3, 18], + requires: Scoring.types.all.value + }) + + ;[[], [''], [null], [Scoring.UNDEFINED], undefined, null, '', Scoring.UNDEFINED] + .forEach(responses => { + const responseDoc = { responses } + const result = scoreChoice(itemDoc, responseDoc) + expect(result).toEqual([{ + competency: itemDoc.scoring[0].competency, + correctResponse: itemDoc.scoring[0].correctResponse, + value: responses === undefined ? [] : responses, + score: false, + isUndefined: true + }]) + }) + }) + it('throws when there is an unknown scoring type', () => { + const itemDoc = createItemDoc({ + flavor: Choice.flavors.multiple.value, + correctResponse: [3, 18], + requires: -99 + }) + const reponseDoc = { responses: ['3', '18'] } + expect(() => scoreChoice(itemDoc, reponseDoc)) + .toThrow('Unexpected scoring type: -99') + }) + + describe(Scoring.types.all.name, () => { + it('scores a truthy multiple choice response (requires all)', () => { + const itemDoc = createItemDoc({ + flavor: Choice.flavors.multiple.value, + correctResponse: [3, 18], + requires: Scoring.types.all.value + }) + const responseDoc = { responses: ['3', '18'] } + const result = scoreChoice(itemDoc, responseDoc) + expect(result).toEqual([{ + competency: itemDoc.scoring[0].competency, + correctResponse: itemDoc.scoring[0].correctResponse, + value: [3, 18].sort(), + score: true, + isUndefined: false + }]) + }) + it('scores a falsy multiple choice response (requires all)', () => { + const itemDoc = createItemDoc({ + flavor: Choice.flavors.multiple.value, + correctResponse: [3, 18], + requires: Scoring.types.all.value + }) + + ;[['3'], ['18'], ['3', '18', '24'], ['2'], ['2', '20'], ['2', '20', '30']] + .forEach(responses => { + const responseDoc = { responses } + const result = scoreChoice(itemDoc, responseDoc) + expect(result).toEqual([{ + competency: itemDoc.scoring[0].competency, + correctResponse: itemDoc.scoring[0].correctResponse, + value: responses.map(toInteger).sort(), + score: false, + isUndefined: false + }]) + }) + }) + }) + + describe(Scoring.types.any.name, () => { + it('scores a truthy multiple choice response (requires any)', () => { + const itemDoc = createItemDoc({ + flavor: Choice.flavors.multiple.value, + correctResponse: [3, 18], + requires: Scoring.types.any.value + }) + + ;[['3'], ['18'], ['3', '15'], [Scoring.UNDEFINED, '3'], ['1', '18', '20']] + .forEach(responses => { + const responseDoc = { responses } + const result = scoreChoice(itemDoc, responseDoc) + expect(result).toEqual([{ + competency: itemDoc.scoring[0].competency, + correctResponse: itemDoc.scoring[0].correctResponse, + value: responses.map(value => { + if (isUndefinedResponse(value)) return undefined + return toInteger(value) + }), + score: true, + isUndefined: false + }]) + }) + }) + it('scores a falsy multiple choice response (requires any)', () => { + const itemDoc = createItemDoc({ + flavor: Choice.flavors.multiple.value, + correctResponse: [3, 18], + requires: Scoring.types.any.value + }) + + ;[['1'], ['20', '23']] + .forEach(responses => { + const responseDoc = { responses } + const result = scoreChoice(itemDoc, responseDoc) + expect(result).toEqual([{ + competency: itemDoc.scoring[0].competency, + correctResponse: itemDoc.scoring[0].correctResponse, + value: responses.map(toInteger).sort(), + score: false, + isUndefined: false + }]) + }) + }) + }) + }) + }) +}) diff --git a/app/__tests__/items/choice/ChoiceRenderer.tests.js b/app/__tests__/items/choice/ChoiceRenderer.tests.js new file mode 100644 index 00000000..80b6e539 --- /dev/null +++ b/app/__tests__/items/choice/ChoiceRenderer.tests.js @@ -0,0 +1,5 @@ +import { ChoiceRenderer } from '../../../lib/items/choice/ChoiceRenderer' + +describe(ChoiceRenderer.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/items/choice/ScoreChoice.tests.js b/app/__tests__/items/choice/ScoreChoice.tests.js new file mode 100644 index 00000000..443555e4 --- /dev/null +++ b/app/__tests__/items/choice/ScoreChoice.tests.js @@ -0,0 +1,5 @@ +import { scoreChoice } from '../../../lib/items/choice/scoring' + +describe(scoreChoice.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/items/choice/getChoiceEntryScoreColor.tests.js b/app/__tests__/items/choice/getChoiceEntryScoreColor.tests.js new file mode 100644 index 00000000..a3a02e4e --- /dev/null +++ b/app/__tests__/items/choice/getChoiceEntryScoreColor.tests.js @@ -0,0 +1,5 @@ +import { getChoiceEntryScoreColor } from '../../../lib/items/choice/getChoiceEntryScoreColor' + +describe(getChoiceEntryScoreColor.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/items/cloze/ClozeRenderer.tests.js b/app/__tests__/items/cloze/ClozeRenderer.tests.js new file mode 100644 index 00000000..dcbd35c2 --- /dev/null +++ b/app/__tests__/items/cloze/ClozeRenderer.tests.js @@ -0,0 +1,5 @@ +import { ClozeRenderer } from '../../../lib/items/cloze/ClozeRenderer' + +describe(ClozeRenderer.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/items/cloze/Clozetokenizer.tests.js b/app/__tests__/items/cloze/Clozetokenizer.tests.js new file mode 100644 index 00000000..e1bcddd4 --- /dev/null +++ b/app/__tests__/items/cloze/Clozetokenizer.tests.js @@ -0,0 +1,691 @@ +import { + ClozeTokenizer, + tokenizeBlanks, + tokenizeSelect, + tokenizeText, + toTokens, + getTokenValueForFlavor +} from '../../../lib/items/cloze/ClozeTokenizer' +import { Cloze } from '../../../lib/items/cloze/Cloze' + +describe('ClozeTokenizer', function () { + describe(getTokenValueForFlavor.name, function () { + it('throws on undefined flavour', function () { + [-1, 0, 5, true, false, {}, undefined, null].forEach(flavor => { + expect(() => getTokenValueForFlavor(flavor)) + .toThrow(`Unexpected flavor: ${flavor}`) + }) + }) + }) + describe(tokenizeBlanks.name, function () { + it('it splits a blanks value into the correct tokens', function () { + const flavor = '99' + + expect(tokenizeBlanks(flavor, '[foo]')).toEqual([{ + hasPre: false, + hasSuf: false, + flavor, + isToken: true, + index: 0, + length: 3, + value: 'foo' + }]) + + expect(tokenizeBlanks(flavor, 'ha [foo] bar')).toEqual([{ + index: 0, + length: 3, + value: 'ha ' + }, { + hasPre: true, + hasSuf: true, + flavor, + isToken: true, + index: 1, + length: 3, + value: 'foo' + }, { + index: 2, + length: 4, + value: ' bar' + }]) + }) + it('it splits a empty value into the correct tokens', function () { + const flavor = '99' + + expect(tokenizeBlanks(flavor, '[foo]')).toEqual([{ + hasPre: false, + hasSuf: false, + flavor, + isToken: true, + index: 0, + length: 3, + value: 'foo' + }]) + + expect(tokenizeBlanks(flavor, 'ha [foo] bar')).toEqual([{ + index: 0, + length: 3, + value: 'ha ' + }, { + hasPre: true, + hasSuf: true, + flavor, + isToken: true, + index: 1, + length: 3, + value: 'foo' + }, { + index: 2, + length: 4, + value: ' bar' + }]) + }) + }) + describe(tokenizeSelect.name, function () { + it('correctly tokenizes a select value', function () { + const flavor = '99' + + expect(tokenizeSelect(flavor, '[foo|bar]')).toEqual([{ + hasPre: false, + hasSuf: false, + flavor, + isToken: true, + index: 0, + length: 7, + value: ['foo', 'bar'] + }]) + + expect(tokenizeSelect(flavor, 'ha [foo|bar|baz] bar')).toEqual([{ + index: 0, + length: 3, + value: 'ha ' + }, { + hasPre: true, + hasSuf: true, + flavor, + isToken: true, + index: 1, + length: 11, + value: ['foo', 'bar', 'baz'] + }, { + index: 2, + length: 4, + value: ' bar' + }]) + }) + }) + describe(tokenizeText.name, function () { + it('it splits a text value into the correct tokens', function () { + const flavor = '99' + + expect(tokenizeText(flavor, '[foo]')).toEqual([{ + hasPre: false, + hasSuf: false, + flavor, + isToken: true, + index: 0, + length: 3, + value: 'foo' + }]) + + expect(tokenizeText(flavor, 'ha [foo] bar')).toEqual([{ + index: 0, + length: 3, + value: 'ha ' + }, { + hasPre: false, + hasSuf: false, + flavor, + isToken: true, + index: 1, + length: 3, + value: 'foo' + }, { + index: 2, + length: 4, + value: ' bar' + }]) + }) + }) + describe(toTokens.name, function () { + it('throws on unexpected flavour', function () { + const flavour = Math.random().toString(16) + expect(() => toTokens({ value: `${flavour}$foo$bar` })) + .toThrow(`Unexpected flavor - ${flavour}`) + }) + it('throws if pattern uses an insufficient syntax', function () { + expect(() => toTokens({ value: 'blanks$[foo]$bar$moo' })) + .toThrow('Invalid options syntax: moo') + expect(() => toTokens({ value: 'blanks$[foo]$bar$moo=' })) + .toThrow('Invalid options syntax: moo') + expect(() => toTokens({ value: 'blanks$[foo]$bar$moo=buya&bla' })) + .toThrow('Invalid options syntax: bla') + }) + it('allows to map splits to renderable tokens', function () { + expect(toTokens({ value: '//' })).toEqual({ + value: '//', + isNewLine: true + }) + expect(toTokens({ value: 'noseparator' })).toEqual({ value: 'noseparator' }) + expect(toTokens({ value: 'blanks$foo$bar' })).toEqual({ + flavor: 2, + isBlock: false, + tts: 'bar', + value: [ + { + index: 0, + length: 3, + value: 'foo' + } + ] + }) + expect(toTokens({ value: 'blanks$[foo]$bar' })).toEqual({ + flavor: 2, + isBlock: false, + tts: 'bar', + value: [ + { + flavor: 2, + hasPre: false, + hasSuf: false, + index: 0, + isToken: true, + length: 3, + value: 'foo' + } + ] + }) + expect(toTokens({ value: 'select$[foo|baz]$bar' })).toEqual({ + flavor: 1, + isBlock: false, + tts: 'bar', + value: [ + { + flavor: 1, + hasPre: false, + hasSuf: false, + index: 0, + isToken: true, + length: 7, + value: ['foo', 'baz'] + } + ] + }) + expect(toTokens({ value: 'empty$[foo]$bar' })).toEqual({ + flavor: 3, + isBlock: false, + tts: 'bar', + value: [{ + flavor: 3, + hasPre: false, + hasSuf: false, + index: 0, + isToken: true, + length: 3, + value: 'foo' + } + ] + }) + expect(toTokens({ value: 'text$[foo]$bar' })).toEqual({ + flavor: 4, + isBlock: false, + tts: 'bar', + value: [ + { + flavor: 4, + hasPre: false, + hasSuf: false, + index: 0, + isToken: true, + length: 3, + value: 'foo' + } + ] + }) + }) + it('supports options but optional', function () { + expect(toTokens({ value: 'blanks$foo$bar$color=primary' })).toEqual({ + flavor: 2, + isBlock: false, + tts: 'bar', + color: 'primary', + value: [ + { + index: 0, + length: 3, + value: 'foo' + } + ] + }) + + // multiple split by & + expect(toTokens({ value: 'blanks$foo$bar$color=primary&border=dark' })).toEqual({ + flavor: 2, + isBlock: false, + tts: 'bar', + color: 'primary', + border: 'dark', + value: [ + { + index: 0, + length: 3, + value: 'foo' + } + ] + }) + }) + }) + + describe(ClozeTokenizer.tokenize.name, function () { + it('tokenizes a default cloze text correctly', function () { + const text = `{{blanks$[L]iebe$Liebe}} Frau Lang, +{{blanks$[L]ara$Lara}} ist {{blanks$[h]eute$heute}} leider krank.` + + const { tokens, tokenIndexes } = ClozeTokenizer.tokenize({ text }) + expect(tokenIndexes).toEqual([0, 1, 2]) + expect(tokens).toEqual([ + { + value: '', + length: 0, + isEmpty: true, + index: 0 + }, + { + isToken: true, + value: [ + { + itemIndex: 0, + isToken: true, + value: 'L', + length: 1, + index: 0, + hasPre: false, + hasSuf: true, + flavor: 2 + }, + { + value: 'iebe', + length: 4, + index: 1 + } + ], + length: 20, + index: 1, + flavor: 2, + tts: 'Liebe', + isBlock: false + }, + { + value: ' Frau Lang, ', + length: 12, + index: 2 + }, + { + isToken: true, + value: '//', + length: 2, + index: 3, + isNewLine: true + }, + { + value: '', + length: 0, + isEmpty: true, + index: 4 + }, + { + isToken: true, + value: [ + { + isToken: true, + value: 'L', + length: 1, + index: 0, + hasPre: false, + hasSuf: true, + flavor: 2, + itemIndex: 1 + }, + { + value: 'ara', + length: 3, + index: 1 + } + ], + length: 18, + index: 5, + flavor: 2, + tts: 'Lara', + isBlock: false + }, + { + value: ' ist ', + length: 5, + index: 6 + }, + { + isToken: true, + value: [ + { + isToken: true, + value: 'h', + length: 1, + index: 0, + hasPre: false, + hasSuf: true, + flavor: 2, + itemIndex: 2 + }, + { + value: 'eute', + length: 4, + index: 1 + } + ], + length: 20, + index: 7, + flavor: 2, + tts: 'heute', + isBlock: false + }, + { + value: ' leider krank.', + length: 14, + index: 8 + } + ]) + }) + it('tokenizes a cloze text in table mode correctly', function () { + const text = `Die Zahl: || 41 || {{blanks$[26]$}} || 19 || {{blanks$[21]$}} || {{blanks$[44]$}} +Das Doppelte: || {{blanks$[82]$}} || 52 || {{blanks$[38]$}} || 42 || 88` + const { tokens, tokenIndexes } = ClozeTokenizer.tokenize({ text, isTable: true }) + expect(tokenIndexes).toEqual([0, 1, 2, 3, 4]) + expect(tokens).toEqual([ + // 1. row + [ + { + value: 'Die Zahl:', + length: 9, + index: 0 + }, + { + value: '41', + length: 2, + index: 1 + }, + { + isToken: true, + value: [ + { + isToken: true, + itemIndex: 0, + value: '26', + length: 2, + index: 0, + hasPre: false, + hasSuf: false, + flavor: 2 + } + ], + length: 12, + index: 2, + flavor: 2, + tts: '', + isBlock: false + }, + { + value: '19', + length: 2, + index: 3 + }, + { // || + isToken: true, + value: [ + { + isToken: true, + value: '21', + itemIndex: 1, + length: 2, + index: 0, + hasPre: false, + hasSuf: false, + flavor: 2 + } + ], + length: 12, + index: 4, + flavor: 2, + tts: '', + isBlock: false + }, + { + isToken: true, + value: [ + { + isToken: true, + value: '44', + itemIndex: 2, + length: 2, + index: 0, + hasPre: false, + hasSuf: false, + flavor: 2 + } + ], + length: 12, + index: 5, + flavor: 2, + tts: '', + isBlock: false + } + ], + // 2. row + [ + { + value: 'Das Doppelte:', + length: 13, + index: 0 + }, + { + isToken: true, + value: [ + { + isToken: true, + value: '82', + itemIndex: 3, + length: 2, + index: 0, + hasPre: false, + hasSuf: false, + flavor: 2 + } + ], + length: 12, + index: 1, + flavor: 2, + tts: '', + isBlock: false + }, + { + value: '52', + length: 2, + index: 2 + }, + { + isToken: true, + value: [ + { + isToken: true, + value: '38', + itemIndex: 4, + length: 2, + index: 0, + hasPre: false, + hasSuf: false, + flavor: 2 + } + ], + length: 12, + index: 3, + flavor: 2, + tts: '', + isBlock: false + }, + { + value: '42', + length: 2, + index: 4 + }, + { + value: '88', + length: 2, + index: 5 + } + ] + ]) + }) + it('tokenizes a cloze table with empties', function () { + const text = `<<>> || 1 || 7 + ++ || 6 || 9 + +<<>> || {{empty$[1]$$pattern=0123456789}} || <<>> + +<<>> || {{blanks$[8]$$cellBorder=top&pattern=0123456789}} || {{blanks$[6]$$cellBorder=top&pattern=0123456789}}` + const { tokens, tokenIndexes } = ClozeTokenizer.tokenize({ text, isTable: true }) + expect(tokenIndexes).toEqual([0, 1, 2]) + expect(tokens).toEqual([ + // 1. row + [ + { + index: 0, + isCellSkip: true, + length: 4, + value: '<<>>' + }, + { + index: 1, + length: 1, + value: '1' + }, + { + index: 2, + length: 1, + value: '7' + } + ], + // 2. row + [ + { + index: 0, + length: 1, + value: '+' + }, + { + index: 1, + length: 1, + value: '6' + }, + { + index: 2, + length: 1, + value: '9' + } + ], + // 3, row + [ + { + index: 0, + isCellSkip: true, + length: 4, + value: '<<>>' + }, + { + index: 1, + flavor: 3, + isBlock: false, + isToken: true, + length: 29, + pattern: '0123456789', + tts: '', + value: [ + { + // empties need an + // itemIndex because scoring + // references targets by index + // that includes empties + itemIndex: 0, + flavor: Cloze.flavor.empty.value, + hasPre: false, + hasSuf: false, + index: 0, + isToken: true, + length: 1, + value: '1' + } + ] + }, + { + index: 2, + isCellSkip: true, + length: 4, + value: '<<>>' + } + ], + // 4. row + [ + { + index: 0, + isCellSkip: true, + length: 4, + value: '<<>>' + }, + { + cellBorder: 'top', + flavor: Cloze.flavor.blanks.value, + index: 1, + isBlock: false, + isToken: true, + length: 45, + pattern: '0123456789', + tts: '', + value: [ + { + itemIndex: 1, + flavor: Cloze.flavor.blanks.value, + hasPre: false, + hasSuf: false, + index: 0, + isToken: true, + length: 1, + value: '8' + } + ] + }, + { + cellBorder: 'top', + flavor: Cloze.flavor.blanks.value, + index: 2, + isBlock: false, + isToken: true, + length: 45, + pattern: '0123456789', + tts: '', + value: [ + { + itemIndex: 2, + flavor: Cloze.flavor.blanks.value, + hasPre: false, + hasSuf: false, + index: 0, + isToken: true, + length: 1, + value: '6' + } + ] + } + ] + ]) + }) + }) +}) diff --git a/app/__tests__/items/cloze/createScoringSummaryForInput.tests.js b/app/__tests__/items/cloze/createScoringSummaryForInput.tests.js new file mode 100644 index 00000000..f455bc5e --- /dev/null +++ b/app/__tests__/items/cloze/createScoringSummaryForInput.tests.js @@ -0,0 +1,52 @@ +import { createScoringSummaryForInput } from '../../../lib/items/cloze/createScoringSummaryForInput' +import { CompareState } from '../../../lib/items/utils/CompareState' +import { UndefinedScore } from '../../../lib/scoring/UndefinedScore' + +describe(createScoringSummaryForInput.name, function () { + it('creates a summary for a single-score response for an input', function () { + ['moo', ['moo'], undefined, [undefined], UndefinedScore, [UndefinedScore]].forEach(value => { + [true, false].forEach(score => { + const expectedColor = CompareState.getColor(score ? 1 : 0) + const entries = [{ value, score: score ? 1 : 0 }] + const summary = createScoringSummaryForInput({ + itemIndex: 5, + actual: value, + entries + }) + expect(summary).toEqual({ + index: 5, + score: score ? 1 : 0, + actual: value, + color: expectedColor, + entries + }) + }) + }) + }) + + it('creates a summary for a multiple-score response for an input', function () { + ['moo', ['moo'], undefined, [undefined], UndefinedScore, [UndefinedScore]].forEach(value => { + [0, 1, 2, 3].forEach(trueScores => { + const avg = trueScores / 3 + const expectedColor = CompareState.getColor(Math.floor(avg)) + const entries = [ + { value, score: trueScores > 0 ? 1 : 0 }, + { value, score: trueScores > 1 ? 1 : 0 }, + { value, score: trueScores > 2 ? 1 : 0 } + ] + const summary = createScoringSummaryForInput({ + itemIndex: 5, + actual: value, + entries + }) + expect(summary).toEqual({ + index: 5, + score: avg, + actual: value, + color: expectedColor, + entries + }) + }) + }) + }) +}) diff --git a/app/__tests__/items/cloze/scoreCloze.tests.js b/app/__tests__/items/cloze/scoreCloze.tests.js new file mode 100644 index 00000000..e0f2a16b --- /dev/null +++ b/app/__tests__/items/cloze/scoreCloze.tests.js @@ -0,0 +1,146 @@ +import { scoreCloze } from '../../../lib/items/cloze/scoring' +import { Scoring } from '../../../lib/scoring/Scoring' +import { simpleRandom } from '../../../__testHelpers__/simpleRandom' + +const createItemDoc = ({ competency, correctResponse } = {}) => { + return { + scoring: [{ + target: 0, + competency: competency ?? [simpleRandom(), simpleRandom()], + correctResponse: correctResponse ?? /.*/ + }, { + target: 1, + competency: competency ?? [simpleRandom(), simpleRandom()], + correctResponse: correctResponse ?? /.*/ + }] + } +} + +describe(scoreCloze.name, function () { + it('detects if all responses are undefined', () => { + const itemDoc = createItemDoc() + const allResponses = [ + [], ['', ''], [undefined, undefined], [null, null], [Scoring.UNDEFINED, Scoring.UNDEFINED] + ] + allResponses.forEach(responses => { + const responseDoc = { responses } + expect(scoreCloze(itemDoc, responseDoc)) + .toEqual([ + { + competency: itemDoc.scoring[0].competency, + correctResponse: itemDoc.scoring[0].correctResponse, + value: responseDoc.responses[0], + score: false, + target: 0, + isUndefined: true + }, + { + competency: itemDoc.scoring[1].competency, + correctResponse: itemDoc.scoring[1].correctResponse, + value: responseDoc.responses[1], + score: false, + target: 1, + isUndefined: true + } + ]) + }) + }) + it('scores correct responses with ture scores', () => { + const itemDoc = createItemDoc({ + correctResponse: /\w+/i + }) + const allResponses = [ + ['foo', 'bar'], [' bar', '\nbaz'], ['lol', 'mooooo#!'] + ] + allResponses.forEach(responses => { + const responseDoc = { responses } + expect(scoreCloze(itemDoc, responseDoc)) + .toEqual([ + { + competency: itemDoc.scoring[0].competency, + correctResponse: itemDoc.scoring[0].correctResponse, + value: responseDoc.responses[0], + score: true, + target: 0, + isUndefined: false + }, + { + competency: itemDoc.scoring[1].competency, + correctResponse: itemDoc.scoring[1].correctResponse, + value: responseDoc.responses[1], + score: true, + target: 1, + isUndefined: false + } + ]) + }) + }) + it('scores mixed true/false responses with respective scores', () => { + const itemDoc = { + scoring: [{ + target: 0, + competency: [simpleRandom(), simpleRandom()], + correctResponse: /^F.*$/ + }, { + target: 0, + competency: [simpleRandom(), simpleRandom()], + correctResponse: /foo/ + }, { + target: 1, + competency: [simpleRandom(), simpleRandom()], + correctResponse: /^bar$/ + }] + } + expect(scoreCloze(itemDoc, { responses: ['foo', 'bar'] })) + .toEqual([{ + competency: itemDoc.scoring[0].competency, + correctResponse: itemDoc.scoring[0].correctResponse, + value: 'foo', + score: false, + target: 0, + isUndefined: false + }, { + competency: itemDoc.scoring[1].competency, + correctResponse: itemDoc.scoring[1].correctResponse, + value: 'foo', + score: true, + target: 0, + isUndefined: false + }, { + competency: itemDoc.scoring[2].competency, + correctResponse: itemDoc.scoring[2].correctResponse, + value: 'bar', + score: true, + target: 1, + isUndefined: false + }]) + }) + it('scores true/undefined responses with respective score', () => { + const itemDoc = createItemDoc() + const allResponses = [ + ['a'], ['foo', ''], ['moo', undefined], ['bar', null], ['baz', Scoring.UNDEFINED] + ] + allResponses.forEach((responses) => { + const responseDoc = { responses } + expect(scoreCloze(itemDoc, responseDoc)) + .toEqual([ + { + competency: itemDoc.scoring[0].competency, + correctResponse: itemDoc.scoring[0].correctResponse, + value: responseDoc.responses[0], + score: true, + target: 0, + isUndefined: false + }, + { + competency: itemDoc.scoring[1].competency, + correctResponse: itemDoc.scoring[1].correctResponse, + value: responseDoc.responses[1], + score: false, + target: 1, + isUndefined: true + } + ]) + }) + }) +}) diff --git a/app/__tests__/items/connect/Connect.tests.js b/app/__tests__/items/connect/Connect.tests.js new file mode 100644 index 00000000..fb000d45 --- /dev/null +++ b/app/__tests__/items/connect/Connect.tests.js @@ -0,0 +1,5 @@ +import { Connect } from '../../../lib/items/connect/Connect' + +describe(Connect.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/items/connect/ConnectItemRenderer.tests.js b/app/__tests__/items/connect/ConnectItemRenderer.tests.js new file mode 100644 index 00000000..22a4dd1d --- /dev/null +++ b/app/__tests__/items/connect/ConnectItemRenderer.tests.js @@ -0,0 +1,5 @@ +import { ConnectItemRenderer } from '../../../lib/items/connect/ConnectItemRenderer' + +describe(ConnectItemRenderer.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/items/connect/ScoreConnect.tests.js b/app/__tests__/items/connect/ScoreConnect.tests.js new file mode 100644 index 00000000..bc32e282 --- /dev/null +++ b/app/__tests__/items/connect/ScoreConnect.tests.js @@ -0,0 +1,5 @@ +import { scoreConnect } from '../../../lib/items/connect/scoreConnect' + +describe(scoreConnect.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/items/highlight/Highlight.tests.js b/app/__tests__/items/highlight/Highlight.tests.js new file mode 100644 index 00000000..c4ce86ba --- /dev/null +++ b/app/__tests__/items/highlight/Highlight.tests.js @@ -0,0 +1,5 @@ +import { Highlight } from '../../../lib/items/highlight/Highlight' + +describe(Highlight.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/items/highlight/HighlightRenderer.tests.js b/app/__tests__/items/highlight/HighlightRenderer.tests.js new file mode 100644 index 00000000..26b04b51 --- /dev/null +++ b/app/__tests__/items/highlight/HighlightRenderer.tests.js @@ -0,0 +1,5 @@ +import { HighlightRenderer } from '../../../lib/items/highlight/HighlightRenderer' + +describe(HighlightRenderer.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/items/highlight/HighlightTokenizer.tests.js b/app/__tests__/items/highlight/HighlightTokenizer.tests.js new file mode 100644 index 00000000..dd82eae3 --- /dev/null +++ b/app/__tests__/items/highlight/HighlightTokenizer.tests.js @@ -0,0 +1,5 @@ +import { HighlightTokenizer } from '../../../lib/items/highlight/HighlightTokenizer' + +describe(HighlightTokenizer.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/items/highlight/scoreHighlight.tests.js b/app/__tests__/items/highlight/scoreHighlight.tests.js new file mode 100644 index 00000000..83006651 --- /dev/null +++ b/app/__tests__/items/highlight/scoreHighlight.tests.js @@ -0,0 +1,5 @@ +import { scoreHighlight } from '../../../lib/items/highlight/scoring' + +describe(scoreHighlight.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/items/shared/getCompareValuesForSelectableItems.test.js b/app/__tests__/items/shared/getCompareValuesForSelectableItems.test.js new file mode 100644 index 00000000..d8691a71 --- /dev/null +++ b/app/__tests__/items/shared/getCompareValuesForSelectableItems.test.js @@ -0,0 +1,22 @@ +import { getCompareValuesForSelectableItems } from '../../../lib/items/shared/getCompareValuesForSelectableItems' + +describe(getCompareValuesForSelectableItems.name, function () { + it('scores correct selections as 1, wrong as 0 and missing as -1', () => { + const inputs = [ + // single values + [{ 0: true }, [0], { 0: 1 }], // correct + [{ 1: true }, [0], { 0: -1, 1: 0 }], // wrong + [{}, [3], { 3: -1 }], // no input + // multiple values + [{ 0: true, 3: true }, [0, 3], { 0: 1, 3: 1 }], // all correct + [{ 1: true, 4: true }, [2, 3], { 1: 0, 2: -1, 3: -1, 4: 0 }], // all wrong + [{ 1: true, 4: true }, [1, 3], { 1: 1, 3: -1, 4: 0 }], // right and wring + [{}, [2, 3], { 2: -1, 3: -1 }] // no input + ] + + inputs.forEach(([selected, correctResponses, expected]) => { + const result = getCompareValuesForSelectableItems({ selected, correctResponses }) + expect(result).toEqual(expected) + }) + }) +}) diff --git a/app/__tests__/items/utils/CompareState.tests.js b/app/__tests__/items/utils/CompareState.tests.js new file mode 100644 index 00000000..428594c3 --- /dev/null +++ b/app/__tests__/items/utils/CompareState.tests.js @@ -0,0 +1,41 @@ +import { CompareState } from '../../../lib/items/utils/CompareState' +import { Colors } from '../../../lib/constants/Colors' +import { simpleRandom } from '../../../__testHelpers__/simpleRandom' +import { Scoring } from '../../../lib/scoring/Scoring' + +describe('CompareState', () => { + describe(CompareState.getColor.name, () => { + it('returns the correct color for given values', () => { + [ + { value: -1, color: Colors.missing }, + { value: 1, color: Colors.right }, + { value: 0, color: Colors.wrong }, + { value: simpleRandom(), color: undefined } + ].forEach(({ value, color }) => { + expect(CompareState.getColor(value)).toEqual(color) + }) + }) + }) + describe(CompareState.getValue.name, function () { + it('returns the correspondig value by given score and response value', () => { + [ + { score: false, value: 'a', expected: 0 }, + { score: false, value: {}, expected: 0 }, + { score: false, value: [], expected: -1 }, + { score: false, value: '', expected: -1 }, + { score: false, value: undefined, expected: -1 }, + { score: false, value: null, expected: -1 }, + { score: false, value: Scoring.UNDEFINED, expected: -1 }, + { score: true, value: '', expected: 1 }, + { score: true, value: 'a', expected: 1 }, + { score: true, value: [], expected: 1 }, + { score: true, value: {}, expected: 1 }, + { score: true, value: undefined, expected: 1 }, + { score: true, value: null, expected: 1 }, + { score: true, value: Scoring.UNDEFINED, expected: 1 } + ].forEach(({ score, value, expected }) => { + expect(CompareState.getValue(score, value)).toEqual(expected) + }) + }) + }) +}) diff --git a/app/__tests__/items/utils/KeyboardTypes.tests.js b/app/__tests__/items/utils/KeyboardTypes.tests.js new file mode 100644 index 00000000..5f319506 --- /dev/null +++ b/app/__tests__/items/utils/KeyboardTypes.tests.js @@ -0,0 +1,13 @@ +import { KeyboardTypes } from '../../../lib/items/utils/KeyboardTypes' + +describe('KeyboardTypes', function () { + describe(KeyboardTypes.get.name, function () { + test.todo('not implemted') + }) + describe(KeyboardTypes.allowedValues.name, function () { + test.todo('not implemted') + }) + describe(KeyboardTypes.register.name, function () { + test.todo('not implemted') + }) +}) diff --git a/app/__tests__/schema/schema.tests.js b/app/__tests__/schema/schema.tests.js new file mode 100644 index 00000000..21f19410 --- /dev/null +++ b/app/__tests__/schema/schema.tests.js @@ -0,0 +1,29 @@ +import { createSchema } from '../../lib/schema/createSchema' +import { isSchemaInstance } from '../../lib/schema/isSchemaInstance' +import { check } from '../../lib/schema/check' + +it('creates a new schema instance', function () { + const schema = createSchema({ foo: String }) + expect(isSchemaInstance(schema)).toBe(true) +}) + +it('uses check to check against a schema', function () { + const schema = createSchema({ foo: String }) + expect(() => check({}, schema)).toThrow('foo is required') + expect(() => check({ foo: 1 }, schema)).toThrow('foo must be of type String') +}) + +it('creates a schema on the fly in check when no schema is passed', function () { + expect(() => check({}, { foo: String })).toThrow('foo is required') + expect(() => check({ foo: 1 }, { foo: String })).toThrow('foo must be of type String') + + const arraySchema = { + foo: { + type: Array, + min: 1 + }, + 'foo.$': String + } + expect(() => check({ foo: [1] }, arraySchema)).toThrow('foo must be of type String') + expect(() => check(1, String)).toThrow('target must be of type String') +}) diff --git a/app/__tests__/schema/settingsSchema.tests.js b/app/__tests__/schema/settingsSchema.tests.js new file mode 100644 index 00000000..e2ccb75a --- /dev/null +++ b/app/__tests__/schema/settingsSchema.tests.js @@ -0,0 +1,10 @@ +import { isSchemaInstance } from '../../lib/schema/isSchemaInstance' +import settings from '../../settings/settings.json' +import { settingsSchema } from '../../lib/settingsSchema' + +describe('settingsSchema', function () { + it('verifies the schema', () => { + expect(isSchemaInstance(settingsSchema)).toBe(true) + settingsSchema.validate(settings) + }) +}) diff --git a/app/__tests__/schema/validateSettingsSchema.tests.js b/app/__tests__/schema/validateSettingsSchema.tests.js new file mode 100644 index 00000000..7c504cd7 --- /dev/null +++ b/app/__tests__/schema/validateSettingsSchema.tests.js @@ -0,0 +1,7 @@ +import { validateSettingsSchema } from '../../lib/schema/validateSettingsSchema' + +describe(validateSettingsSchema.name, function () { + it('validates the settings schema', () => { + validateSettingsSchema() + }) +}) diff --git a/app/__tests__/scoring/Scoring.tests.js b/app/__tests__/scoring/Scoring.tests.js new file mode 100644 index 00000000..a82407e2 --- /dev/null +++ b/app/__tests__/scoring/Scoring.tests.js @@ -0,0 +1,48 @@ +import { Scoring } from '../../lib/scoring/Scoring' +import { simpleRandom } from '../../__testHelpers__/simpleRandom' +import { expectThrowAsync } from '../../__testHelpers__/expectThrowAsync' + +describe(Scoring.name, function () { + describe(Scoring.score.name, () => { + it('throws if an invalid itemDoc is given', async () => { + await expectThrowAsync({ + fn: () => Scoring.score(), + message: 'Expected itemDoc to have property "scoring"' + }) + await expectThrowAsync({ + fn: () => Scoring.score({}), + message: 'Expected itemDoc to have property "scoring"' + }) + }) + it('throws if an invalid responseDoc is given', async () => { + await expectThrowAsync({ + fn: () => Scoring.score({ scoring: [{}] }), + message: 'Expected responseDoc, got undefined' + }) + await expectThrowAsync({ + fn: () => Scoring.score({ scoring: [{}] }, {}), + message: 'Expected responses to have Array-like property "responses"' + }) + }) + it('throws if there is no scoring handler found by options', async () => { + const type = simpleRandom() + const subtype = simpleRandom() + const options = { type, subtype, scoring: [{}] } + + await expectThrowAsync({ + fn: () => Scoring.score(options, { responses: [''] }), + message: `Expected scoring fn by ${type} / ${subtype}` + }) + }) + it('executes the respective registered scoring handler', async () => { + const type = simpleRandom() + const subtype = simpleRandom() + const expectedResult = simpleRandom() + const scoreFn = () => expectedResult + const options = { type, subtype, scoring: [{}] } + Scoring.register({ type, subtype, scoreFn }) + const result = await Scoring.score(options, { responses: [] }) + expect(result).toEqual(expectedResult) + }) + }) +}) diff --git a/app/__tests__/scoring/getScoring.tests.js b/app/__tests__/scoring/getScoring.tests.js new file mode 100644 index 00000000..2c175813 --- /dev/null +++ b/app/__tests__/scoring/getScoring.tests.js @@ -0,0 +1,5 @@ +import { getScoring } from '../../lib/scoring/getScoring' + +describe(getScoring.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/screens/BaseScreen.tests.js b/app/__tests__/screens/BaseScreen.tests.js new file mode 100644 index 00000000..3f946848 --- /dev/null +++ b/app/__tests__/screens/BaseScreen.tests.js @@ -0,0 +1,5 @@ +import { ScreenBase } from '../../lib/screens/BaseScreen' + +describe(ScreenBase.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/screens/complete/Celebrate.tests.js b/app/__tests__/screens/complete/Celebrate.tests.js new file mode 100644 index 00000000..0dd24d8f --- /dev/null +++ b/app/__tests__/screens/complete/Celebrate.tests.js @@ -0,0 +1,5 @@ +import { Celebrate } from '../../../lib/screens/complete/Celebrate' + +describe(Celebrate.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/screens/complete/CompleteScreen.tests.js b/app/__tests__/screens/complete/CompleteScreen.tests.js new file mode 100644 index 00000000..aa4562ec --- /dev/null +++ b/app/__tests__/screens/complete/CompleteScreen.tests.js @@ -0,0 +1,5 @@ +import { CompleteScreen } from '../../../lib/screens/complete/CompleteScreen' + +describe(CompleteScreen.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/screens/complete/generateFeedback.tests.js b/app/__tests__/screens/complete/generateFeedback.tests.js new file mode 100644 index 00000000..76d9e237 --- /dev/null +++ b/app/__tests__/screens/complete/generateFeedback.tests.js @@ -0,0 +1,48 @@ +import { generateFeedback } from '../../../lib/screens/complete/generateFeedback' +import { Feedback } from '../../../lib/contexts/Feedback' + +const fallback = Feedback.getFallbackDoc() + +describe(generateFeedback.name, function () { + it('returns a fallback doc if nothing is found', () => { + const data = [{}, { threshold: 0 }, { feedbackDocs: [] }, { threshold: 0, feedbackDocs: [] }] + data.forEach(({ threshold, feedbackDocs }) => { + const feedback = generateFeedback({ threshold, feedbackDocs }) + expect(feedback.percent).toBe(0) + expect(feedback.isFallback).toBe(true) + expect(feedback.phrase).toBe(fallback.phrases[0]) + }) + }) + it('falls back to the first doc in list, if no doc is suitable for the threshold', () => { + const feedbackDocs = [{ threshold: 0.2, phrases: ['foo'] }, { threshold: 0.5, phrases: ['bar'] }] + const data = [{}, { threshold: 0 }, { threshold: 0.1 }] + + data.forEach(({ threshold }) => { + const feedback = generateFeedback({ threshold, feedbackDocs }) + expect(feedback.percent).toBe(Math.round((threshold ?? 0) * 100)) + expect(feedback.phrase).toBe('foo') + expect(feedback.isFallback).toBe(false) + }) + }) + + it('returns the appropriate feedback for the current threshold', () => { + const feedbackDocs = [ + { threshold: 0.2, phrases: ['foo', 'bar'] }, + { threshold: 0.5, phrases: ['baz', 'moo'] } + ] + + for (let i = 0.2; i < 0.5; i += 0.01) { + const feedback = generateFeedback({ threshold: i, feedbackDocs }) + expect(feedback.percent).toEqual(Math.round(i * 100)) + expect(feedbackDocs[0].phrases.includes(feedback.phrase)).toBe(true) + expect(feedback.isFallback).toBe(false) + } + + for (let i = 0.5; i <= 1; i += 0.01) { + const feedback = generateFeedback({ threshold: i, feedbackDocs }) + expect(feedback.percent).toEqual(Math.round(i * 100)) + expect(feedbackDocs[1].phrases.includes(feedback.phrase)).toBe(true) + expect(feedback.isFallback).toBe(false) + } + }) +}) diff --git a/app/__tests__/screens/complete/loadCompleteData.tests.js b/app/__tests__/screens/complete/loadCompleteData.tests.js new file mode 100644 index 00000000..77343ceb --- /dev/null +++ b/app/__tests__/screens/complete/loadCompleteData.tests.js @@ -0,0 +1,5 @@ +import { loadCompleteData } from '../../../lib/screens/complete/loadCompleteData' + +describe(loadCompleteData.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/screens/home/HomeScreen.tests.js b/app/__tests__/screens/home/HomeScreen.tests.js new file mode 100644 index 00000000..9a6b3cf1 --- /dev/null +++ b/app/__tests__/screens/home/HomeScreen.tests.js @@ -0,0 +1,5 @@ +import { HomeScreen } from '../../../lib/screens/home/HomeScreen' + +describe(HomeScreen.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/screens/home/laodHomeData.tests.js b/app/__tests__/screens/home/laodHomeData.tests.js new file mode 100644 index 00000000..9a6b3cf1 --- /dev/null +++ b/app/__tests__/screens/home/laodHomeData.tests.js @@ -0,0 +1,5 @@ +import { HomeScreen } from '../../../lib/screens/home/HomeScreen' + +describe(HomeScreen.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/screens/map/loadMapData.tests.js b/app/__tests__/screens/map/loadMapData.tests.js new file mode 100644 index 00000000..83aae763 --- /dev/null +++ b/app/__tests__/screens/map/loadMapData.tests.js @@ -0,0 +1,261 @@ +import Meteor from '@meteorrn/core' +import { Dimension } from '../../../lib/contexts/Dimension' +import { stub, restoreAll } from '../../../__testHelpers__/stub' +import { simpleRandom } from '../../../__testHelpers__/simpleRandom' +import { loadMapData } from '../../../lib/screens/map/loadMapData' +import { toDocId } from '../../../lib/utils/array/toDocId' +import { mockCollection, resetCollection, restoreCollection } from '../../../__testHelpers__/mockCollection' +import { byDocId } from '../../../lib/utils/array/byDocId' +import { MapIcons } from '../../../lib/contexts/MapIcons' +import { Order } from '../../../lib/contexts/Order' +import { mockCall } from '../../../__testHelpers__/mockCall' + +describe(loadMapData.name, () => { + beforeAll(() => { + mockCollection(Dimension) + mockCollection(Order) + mockCollection(MapIcons) + }) + + afterEach(() => { + restoreAll() + resetCollection(Dimension) + resetCollection(Order) + resetCollection(MapIcons) + }) + + afterAll(() => { + restoreCollection(Dimension) + restoreCollection(Order) + restoreCollection(MapIcons) + }) + + it('returns an "empty" message if the server responded with no or faulty map data', async () => { + const fieldDoc = { _id: simpleRandom() } + + + const allData = [ + undefined, + null, + {}, + { dimensions: [{}] }, + { dimensions: [{}], entries: [{}] }, + { levels: [{}], entries: [{}] }, + { dimensions: [{}], levels: [{}] } + ] + + let index = 0 + + mockCall((name, args, cb) => cb(undefined, allData[index++])) + + for (const input of allData) { + const data = await loadMapData({ fieldDoc, input }) + expect(data).toEqual({ empty: true }) + } + }) + + it('loads the map data without user progress', async () => { + const fieldDoc = { _id: simpleRandom(), title: simpleRandom() } + const dimensions = [ + { _id: simpleRandom(), title: simpleRandom(), shortCode: 'R' }, + { _id: simpleRandom(), title: simpleRandom(), shortCode: 'W' } + ] + + const DimensionCollection = Dimension.collection() + + stub(DimensionCollection, 'findOne', (_id) => { + return dimensions.find((byDocId(_id))) + }) + + const levels = [ + { _id: simpleRandom() }, + { _id: simpleRandom() } + ] + + const mapData = { + dimensions: dimensions.map(doc => ({ _id: doc._id, maxProgress: 123, maxCompetencies: 456 })), + levels: levels.map(toDocId), + entries: [{}] + } + + mockCall((name, args, cb) => setTimeout(() => cb(undefined, mapData))) + + const data = await loadMapData({ fieldDoc, loadUserData: null }) + expect(data.fieldName).toEqual(fieldDoc.title) + expect(data.dimensionsResolved).toEqual(true) + expect(data.dimensions).toEqual(dimensions) + expect(data.levels).toEqual(levels.map(toDocId)) + }) + + it('caches the map, once loaded', async () => { + const fieldDoc = { _id: simpleRandom(), title: simpleRandom() } + const dimensions = [ + { _id: simpleRandom(), title: simpleRandom(), shortCode: 'R' }, + { _id: simpleRandom(), title: simpleRandom(), shortCode: 'W' } + ] + + const DimensionCollection = Dimension.collection() + + stub(DimensionCollection, 'findOne', (_id) => { + return dimensions.find((byDocId(_id))) + }) + + const levels = [ + { _id: simpleRandom() }, + { _id: simpleRandom() } + ] + + const mapData = { + viewElementsAdded: false, + dimensionsResolved: false, + dimensions: dimensions.map(toDocId), + levels: levels.map(toDocId), + entries: [{}] + } + + let callCount = 0 + mockCall((name, args, cb) => { + callCount++ + mapData.viewElementsAdded = true + mapData.dimensionsResolved = true + setTimeout(() => cb(undefined, mapData)) + }) + + const data1 = await loadMapData({ fieldDoc, loadUserData: null }) + const data2 = await loadMapData({ fieldDoc, loadUserData: null }) + expect(callCount).toEqual(1) + expect(data1).toEqual(data2) + }) + + it('adds additional rendering information to the entries', async () => { + const fieldDoc = { _id: simpleRandom(), title: simpleRandom() } + const dimensions = [ + { _id: simpleRandom(), title: simpleRandom(), shortCode: 'R' }, + { _id: simpleRandom(), title: simpleRandom(), shortCode: 'W' } + ] + + const DimensionCollection = Dimension.collection() + + stub(DimensionCollection, 'findOne', (_id) => { + return dimensions.find((byDocId(_id))) + }) + + const levels = [ + { _id: simpleRandom() }, + { _id: simpleRandom() } + ] + + stub(MapIcons.collection(), 'findOne', () => ({ + fieldId: fieldDoc._id, + icons: ['foo', 'bar'] + })) + + MapIcons.setField(fieldDoc._id) + + const mapData = { + dimensions: dimensions.map(toDocId), + levels: levels.map(toDocId), + entries: [ + { + type: 'stage' + }, + { + type: 'stage' + }, + { + type: 'milestone' + }, + { + type: 'stage' + }, + { + type: 'stage' + }, + { + type: 'milestone' + } + ] + } + mockCall((name, args, cb) => setTimeout(() => cb(undefined, mapData))) + + const { entries } = await loadMapData({ fieldDoc, loadUserData: null }) + + // first + expect(entries[0]).toEqual({ + type: 'start', + entryKey: 'map-entry-0', + viewPosition: { + icon: 0, + current: 'center', + left: 'fill', + right: 'left2right-up' + } + }) + + // when next is a stage then + // we display a connector on the opposite + // side of the entry + expect(entries[1]).toEqual({ + type: 'stage', + entryKey: 'map-entry-1', + label: 1, + viewPosition: { + icon: 1, + left: 'right2left', + current: 'right', + right: null + } + }) + + // when next is not stage then there + // are no connectors and no icon + expect(entries[2]).toEqual({ + type: 'stage', + entryKey: 'map-entry-2', + label: 2, + viewPosition: { + icon: -1, + left: null, + current: 'left', + right: null + } + }) + + // milestones are always centered + expect(entries[3]).toEqual({ + type: 'milestone', + entryKey: 'map-entry-3', + viewPosition: { + left: 'right2left-down', + current: 'center', + right: 'left2right-up' + } + }) + + // another stage + expect(entries[4]).toEqual({ + type: 'stage', + entryKey: 'map-entry-4', + label: 3, + viewPosition: { + icon: 0, // index begins again at 0 + left: 'right2left', + current: 'right', + right: null + } + }) + + // last + expect(entries[6]).toEqual({ + type: 'finish', + entryKey: 'map-entry-6', + viewPosition: { + current: 'center', + left: 'right2left-down', + right: 'fill' + } + }) + }) + test.todo('loads the map data with user progress added, if given') + test.todo('updates the user data into the cached map') +}) diff --git a/app/__tests__/startup/createSessionValidator.tests.js b/app/__tests__/startup/createSessionValidator.tests.js new file mode 100644 index 00000000..f6c7b29b --- /dev/null +++ b/app/__tests__/startup/createSessionValidator.tests.js @@ -0,0 +1,5 @@ +import { createSessionValidator } from '../../lib/startup/createSessionValidator' + +describe(createSessionValidator.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/startup/initAppSession.tests.js b/app/__tests__/startup/initAppSession.tests.js new file mode 100644 index 00000000..e56af2d8 --- /dev/null +++ b/app/__tests__/startup/initAppSession.tests.js @@ -0,0 +1,5 @@ +import { initAppSession } from '../../lib/startup/initAppSession' + +describe(initAppSession.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/startup/initContexts.tests.js b/app/__tests__/startup/initContexts.tests.js new file mode 100644 index 00000000..c124f773 --- /dev/null +++ b/app/__tests__/startup/initContexts.tests.js @@ -0,0 +1,5 @@ +import { initContexts } from '../../lib/startup/initContexts' + +describe(initContexts.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/startup/initExceptionHandling.tests.js b/app/__tests__/startup/initExceptionHandling.tests.js new file mode 100644 index 00000000..36457266 --- /dev/null +++ b/app/__tests__/startup/initExceptionHandling.tests.js @@ -0,0 +1,5 @@ +import { initExceptionHandling } from '../../lib/startup/initExceptionHandling' + +describe(initExceptionHandling.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/startup/initSound.tests.js b/app/__tests__/startup/initSound.tests.js new file mode 100644 index 00000000..a7a2e0b4 --- /dev/null +++ b/app/__tests__/startup/initSound.tests.js @@ -0,0 +1,5 @@ +import { initSound } from '../../lib/startup/initSound' + +describe(initSound.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/startup/initTts.tests.js b/app/__tests__/startup/initTts.tests.js new file mode 100644 index 00000000..e8764bf5 --- /dev/null +++ b/app/__tests__/startup/initTts.tests.js @@ -0,0 +1,5 @@ +import { initTTs } from '../../lib/startup/initTTS' + +describe(initTTs.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/state/AppSession.tests.js b/app/__tests__/state/AppSession.tests.js new file mode 100644 index 00000000..7cd9b5c6 --- /dev/null +++ b/app/__tests__/state/AppSession.tests.js @@ -0,0 +1,5 @@ +import { AppSession } from '../../lib/state/AppSession' + +describe(AppSession.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/styles/createStyleSheet.tests.js b/app/__tests__/styles/createStyleSheet.tests.js new file mode 100644 index 00000000..b6be8c97 --- /dev/null +++ b/app/__tests__/styles/createStyleSheet.tests.js @@ -0,0 +1,5 @@ +import { createStyleSheet } from '../../lib/styles/createStyleSheet' + +describe(createStyleSheet.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/styles/makeTransparent.tests.js b/app/__tests__/styles/makeTransparent.tests.js new file mode 100644 index 00000000..42a958b6 --- /dev/null +++ b/app/__tests__/styles/makeTransparent.tests.js @@ -0,0 +1,5 @@ +import { makeTransparent } from '../../lib/styles/makeTransparent' + +describe(makeTransparent.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/styles/mergeStyles.tests.js b/app/__tests__/styles/mergeStyles.tests.js new file mode 100644 index 00000000..8de4c27a --- /dev/null +++ b/app/__tests__/styles/mergeStyles.tests.js @@ -0,0 +1,5 @@ +import { mergeStyles } from '../../lib/styles/mergeStyles' + +describe(mergeStyles.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/tts/TTSSpeedConfig.tests.js b/app/__tests__/tts/TTSSpeedConfig.tests.js new file mode 100644 index 00000000..2744703d --- /dev/null +++ b/app/__tests__/tts/TTSSpeedConfig.tests.js @@ -0,0 +1,5 @@ +import { TTSSpeedConfig } from '../../lib/tts/TTSSpeedConfig' + +describe(TTSSpeedConfig.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/tts/TTSVoiceConfig.tests.js b/app/__tests__/tts/TTSVoiceConfig.tests.js new file mode 100644 index 00000000..51a0a480 --- /dev/null +++ b/app/__tests__/tts/TTSVoiceConfig.tests.js @@ -0,0 +1,5 @@ +import { TTSVoiceConfig } from '../../lib/tts/TTSVoiceConfig' + +describe(TTSVoiceConfig.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/utils/array/byDocId.tests.js b/app/__tests__/utils/array/byDocId.tests.js new file mode 100644 index 00000000..2f2d85f3 --- /dev/null +++ b/app/__tests__/utils/array/byDocId.tests.js @@ -0,0 +1,17 @@ +import { byDocId } from '../../../lib/utils/array/byDocId' + +describe(byDocId.name, function () { + it('helps to find the doc id in target', () => { + const _id = 'foo' + const matcher = byDocId(_id) + expect(matcher({ _id })).toBe(true) + expect(matcher({ _id: 'bar' })).toBe(false) + expect(matcher({})).toBe(false) + expect(matcher([])).toBe(false) + expect(matcher(new Date())).toBe(false) + expect(matcher()).toBe(false) + + const filtered = [{ _id }, { _id }, { _id: 'bar' }, { _id }, { _id }].filter(matcher) + expect(filtered.length).toBe(4) + }) +}) diff --git a/app/__tests__/utils/array/byOrderedIds.tests.js b/app/__tests__/utils/array/byOrderedIds.tests.js new file mode 100644 index 00000000..e1dbfa34 --- /dev/null +++ b/app/__tests__/utils/array/byOrderedIds.tests.js @@ -0,0 +1,33 @@ +import { byOrderedIds } from '../../../lib/utils/array/byOrderedIds' + +describe(byOrderedIds.name, function () { + it('throws if ids are not found in the given list of ids [empty list]', () => { + const sorter = byOrderedIds() + const a = { _id: 'foo' } + const b = { _id: 'bar' } + expect(() => sorter(a, b)) + .toThrow('Expected foo and bar to not result in -1 and -1') + }) + it('throws if ids are not found in the given list of ids [a not found]', () => { + const sorter = byOrderedIds(['bar', 'baz']) + const a = { _id: 'foo' } + const b = { _id: 'bar' } + expect(() => sorter(a, b)) + .toThrow('Expected foo and bar to not result in -1 and 0') + }) + it('throws if ids are not found in the given list of ids [b not found]', () => { + const sorter = byOrderedIds(['foo', 'baz']) + const a = { _id: 'foo' } + const b = { _id: 'bar' } + expect(() => sorter(a, b)) + .toThrow('Expected foo and bar to not result in 0 and -1') + }) + it('sorts by given ids', () => { + const sorter = byOrderedIds(['foo', 'bar', 'baz', 'moo']) + const sorted = [{ _id: 'moo' }, { _id: 'foo' }, { _id: 'baz' }, { _id: 'bar' }] + sorted.sort(sorter) + expect(sorted).toStrictEqual([ + { _id: 'foo' }, { _id: 'bar' }, { _id: 'baz' }, { _id: 'moo' } + ]) + }) +}) diff --git a/app/__tests__/utils/array/randomArrayElement.tests.js b/app/__tests__/utils/array/randomArrayElement.tests.js new file mode 100644 index 00000000..bfe2a5ad --- /dev/null +++ b/app/__tests__/utils/array/randomArrayElement.tests.js @@ -0,0 +1,32 @@ +import { randomArrayElement } from '../../../lib/utils/array/randomArrayElement' + +jest.retryTimes(1) + +describe(randomArrayElement.name, function () { + it('returns the first element in 0 or 1 length arrays', () => { + expect(randomArrayElement([])).toBe(undefined) + expect(randomArrayElement([0])).toBe(0) + }) + it('throws if given value is no arary', () => { + [{}, () => {}, new Date(), 1, '1', false, undefined, null] + .forEach(value => { + expect(() => randomArrayElement(value)) + .toThrow(`Expected array, got ${value}`) + }) + }) + it('returns a random element from an array', () => { + const input = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + const covered = new Set() + for (let i = 0; i < 1000; i++) { + const element = randomArrayElement(input) + covered.add(element) + const index = input.indexOf(element) + expect(index).toBeGreaterThan(-1) + } + + // there might be the chance of failing, + // but it's super low! + // still we use retryTimes to cover this + expect(covered.size).toBe(input.length) + }) +}) diff --git a/app/__tests__/utils/array/toArrayIfNot.tests.js b/app/__tests__/utils/array/toArrayIfNot.tests.js new file mode 100644 index 00000000..81e3475d --- /dev/null +++ b/app/__tests__/utils/array/toArrayIfNot.tests.js @@ -0,0 +1,20 @@ +import { toArrayIfNot } from '../../../lib/utils/array/toArrayIfNot' + +describe(toArrayIfNot.name, function () { + it('returns an empty array if undefined is passed', () => { + expect(toArrayIfNot()).toStrictEqual([]) + }) + it('returns an array of a single non-array value/object', () => { + [() => {}, {}, 1, '1', false, true, 0.1 + 0.2, null] + .forEach(value => { + expect(toArrayIfNot(value)) + .toStrictEqual([value]) + }) + }) + it('returns the array if it is an array', () => { + [[() => {}], [{}], [1], ['1'], [false], [true], [0.1 + 0.2], [null]] + .forEach(array => { + expect(toArrayIfNot(array) === array).toBe(true) + }) + }) +}) diff --git a/app/__tests__/utils/array/toDocId.tests.js b/app/__tests__/utils/array/toDocId.tests.js new file mode 100644 index 00000000..27490832 --- /dev/null +++ b/app/__tests__/utils/array/toDocId.tests.js @@ -0,0 +1,8 @@ +import { toDocId } from '../../../lib/utils/array/toDocId' + +describe(toDocId.name, function () { + it('maps objects to their doc ids', () => { + expect([{ _id: 'foo' }, { _id: 'bar' }].map(toDocId)) + .toStrictEqual(['foo', 'bar']) + }) +}) diff --git a/app/__tests__/utils/createTimesPromise.tests.js b/app/__tests__/utils/createTimesPromise.tests.js new file mode 100644 index 00000000..a9507f70 --- /dev/null +++ b/app/__tests__/utils/createTimesPromise.tests.js @@ -0,0 +1,43 @@ +import { createTimedPromise } from '../../lib/utils/createTimedPromise' + +const createPromise = (timeout, message = 'foo') => new Promise(resolve => { + const timer = setTimeout(() => { + clearTimeout(timer) + resolve(message) + }, timeout) +}) + +describe(createTimedPromise.name, function () { + beforeAll(() => { + jest.useFakeTimers({ advanceTimers: true }) + }) + it('resolves to the promise if it resolves faster', async () => { + const myPromise = createPromise(250, 'foo1') + const value = await createTimedPromise(myPromise) + expect(value).toEqual('foo1') + }) + it('resolves to the fallback promise if it resolves not fast enough', async () => { + const myPromise = createPromise(1250) + const message = 'bar1' + const timeout = 1000 + const options = { message, timeout } + const value = await createTimedPromise(myPromise, options) + expect(value).toEqual('bar1') + }) + it('rejects the fallback promise if it resolves not fast enough and displays a custom message', () => { + const myPromise1 = createPromise(1250) + const message = 'bar2' + const timeout = 1000 + const options = { message, timeout, throwIfTimedOut: true } + const race = createTimedPromise(myPromise1, options) + expect(race).rejects.toThrow('bar2') + }) + it('rejects the fallback promise if it resolves not fast enough and displays custom details', () => { + const myPromise2 = createPromise(1250) + const options = { throwIfTimedOut: true, details: { bar: 'baz1' }, timeout: 1000 } + const race = createTimedPromise(myPromise2, options) + const target = expect(race).rejects + target.toThrow('promise.timedOut') + target.toHaveProperty('details', options.details) + }) +}) diff --git a/app/__tests__/utils/isOS.tests.js b/app/__tests__/utils/isOS.tests.js new file mode 100644 index 00000000..b8163c91 --- /dev/null +++ b/app/__tests__/utils/isOS.tests.js @@ -0,0 +1,5 @@ +import { isIOS } from '../../lib/utils/isIOS' + +describe(isIOS.name, function () { + test.todo('it is not impl') +}) diff --git a/app/__tests__/utils/math/average.tests.js b/app/__tests__/utils/math/average.tests.js new file mode 100644 index 00000000..6d8860f2 --- /dev/null +++ b/app/__tests__/utils/math/average.tests.js @@ -0,0 +1,16 @@ +import { average } from '../../../lib/utils/math/average' + +describe(average.name, function () { + it('computes the average between two values', () => { + [ + [0, 0, 0], + [0, 1, 0], + [1, 2, 0.5], + [1, 3, 1 / 3], + [-1, 3, 0], + [-1, -1, 0] + ].forEach(([sum, max, expected]) => { + expect(average(sum, max)).toBe(expected) + }) + }) +}) diff --git a/app/__tests__/utils/math/getPositionOnCircle.tests.js b/app/__tests__/utils/math/getPositionOnCircle.tests.js new file mode 100644 index 00000000..c93bfc6e --- /dev/null +++ b/app/__tests__/utils/math/getPositionOnCircle.tests.js @@ -0,0 +1,21 @@ +import { getPositionOnCircle } from '../../../lib/utils/trigonometry/getPositionOnCircle' + +describe(getPositionOnCircle.name, function () { + it('returns the n equally distributed positions on a circle by given radius', () => { + const positions = getPositionOnCircle({ n: 3, radius: 1, precision: 5 }) + expect(positions).toStrictEqual([ + { + x: 2, + y: 1 + }, + { + x: 0.5, + y: 1.866 + }, + { + x: 0.5, + y: 0.13397 + } + ]) + }) +}) diff --git a/app/__tests__/utils/math/randomInclusive.tests.js b/app/__tests__/utils/math/randomInclusive.tests.js new file mode 100644 index 00000000..ba5685d6 --- /dev/null +++ b/app/__tests__/utils/math/randomInclusive.tests.js @@ -0,0 +1,14 @@ +import { randomIntInclusive } from '../../../lib/utils/math/randomIntInclusive' +import { getInvalidIntegers } from '../../../__testHelpers__/getInvalidIntegers' + +describe(randomIntInclusive.name, function () { + it('throws if one or both numbers are not valid ints', () => { + const invalid = getInvalidIntegers() + invalid.forEach(value => { + expect(() => randomIntInclusive(value, 1)) + .toThrow(`Expected safe integers, got ${value} and 1`) + expect(() => randomIntInclusive(1, value)) + .toThrow(`Expected safe integers, got 1 and ${value}`) + }) + }) +}) diff --git a/app/__tests__/utils/number/isSafeInteger.tests.js b/app/__tests__/utils/number/isSafeInteger.tests.js new file mode 100644 index 00000000..787de2a0 --- /dev/null +++ b/app/__tests__/utils/number/isSafeInteger.tests.js @@ -0,0 +1,17 @@ +import { isSafeInteger } from '../../../lib/utils/number/isSafeInteger' +import { getInvalidIntegers } from '../../../__testHelpers__/getInvalidIntegers' + +describe(isSafeInteger.name, function () { + it('returns false for invalid integers', () => { + const invalid = getInvalidIntegers() + invalid.forEach(value => { + expect(isSafeInteger(value)).toBe(false) + }) + }) + it('returns true on valid integers', () => { + const valid = [1, 2, 3, 0, -1, -2, 1.0, -3.0] + valid.forEach(value => { + expect(isSafeInteger(value)).toBe(true) + }) + }) +}) diff --git a/app/__tests__/utils/number/isValidNumber.tests.js b/app/__tests__/utils/number/isValidNumber.tests.js new file mode 100644 index 00000000..fdc12bea --- /dev/null +++ b/app/__tests__/utils/number/isValidNumber.tests.js @@ -0,0 +1,11 @@ +import { isValidNumber } from '../../../lib/utils/number/isValidNumber' +import { getInvalidNumbers } from '../../../__testHelpers__/getInvalidNumbers' + +describe(isValidNumber.name, function () { + it('returns false on invalid integers', () => { + const invalid = getInvalidNumbers() + invalid.forEach(value => { + expect(isValidNumber(value)).toBe(false) + }) + }) +}) diff --git a/app/__tests__/utils/number/toInteger.tests.js b/app/__tests__/utils/number/toInteger.tests.js new file mode 100644 index 00000000..d0352500 --- /dev/null +++ b/app/__tests__/utils/number/toInteger.tests.js @@ -0,0 +1,16 @@ +import { toInteger } from '../../../lib/utils/number/toInteger' + +describe(toInteger.name, function () { + it('parses string to an integer', () => { + [ + ['1', 1], + ['1.0', 1], + ['1.1', 1], + ['-1', -1], + ['-1.0', -1], + ['-1.1', -1] + ].forEach(([n, expected]) => { + expect(toInteger(n)).toBe(expected) + }) + }) +}) diff --git a/app/__tests__/utils/object/clearObject.tests.js b/app/__tests__/utils/object/clearObject.tests.js new file mode 100644 index 00000000..15c60415 --- /dev/null +++ b/app/__tests__/utils/object/clearObject.tests.js @@ -0,0 +1,17 @@ +import { clearObject } from '../../../lib/utils/object/clearObject' + +describe(clearObject.name, function () { + it('removes all own properties of an object', () => { + const obj = { foo: 'bar' } + clearObject(obj) + expect(obj).toStrictEqual({}) + expect(typeof obj.toString).toBe('function') + }) + it('throws if obj is not an object', () => { + [[], null, undefined, 1, 1.2, '1', false, true, () => {}] + .forEach(value => { + expect(() => clearObject(value)) + .toThrow(`Expected objected, got ${typeof value}`) + }) + }) +}) diff --git a/app/__tests__/utils/object/hasOwnProps.tests.js b/app/__tests__/utils/object/hasOwnProps.tests.js new file mode 100644 index 00000000..0fb6385b --- /dev/null +++ b/app/__tests__/utils/object/hasOwnProps.tests.js @@ -0,0 +1,14 @@ +import { hasOwnProp } from '../../../lib/utils/object/hasOwnProp' + +describe(hasOwnProp.name, function () { + it('returns true only for own props', () => { + const obj = { foo: 'bar' } + expect(hasOwnProp(obj, 'foo')).toBe(true) + + class A { + foo () { return 1 } + } + const a = new A() + expect(hasOwnProp(a, 'foo')).toBe(false) + }) +}) diff --git a/app/__tests__/utils/object/isDefined.tests.js b/app/__tests__/utils/object/isDefined.tests.js new file mode 100644 index 00000000..063931f8 --- /dev/null +++ b/app/__tests__/utils/object/isDefined.tests.js @@ -0,0 +1,10 @@ +import { isDefined } from '../../../lib/utils/object/isDefined' + +describe(isDefined.name, function () { + it('returns only true if something is not undefined and not null', () => { + [undefined, null].forEach(val => expect(isDefined(val)).toBe(false)) + + ;[true, false, 'a', 1, 0, '0', () => {}, []] + .forEach(val => expect(isDefined(val)).toBe(true)) + }) +}) diff --git a/app/__tests__/utils/text/createSimpleTokenizer.tests.js b/app/__tests__/utils/text/createSimpleTokenizer.tests.js new file mode 100644 index 00000000..ac580866 --- /dev/null +++ b/app/__tests__/utils/text/createSimpleTokenizer.tests.js @@ -0,0 +1,12 @@ +import { createSimpleTokenizer } from '../../../lib/utils/text/createSimpleTokenizer' + +describe(createSimpleTokenizer.name, function () { + it('returns an empty array if input is not a string of length > 0', () => { + const tokenize = createSimpleTokenizer('[', ']') + + ;[null, undefined, false, true, '', 0, 1, {}, [], () => {}] + .forEach(val => { + expect(tokenize(val)).toStrictEqual([]) + }) + }) +}) diff --git a/app/__tests__/utils/text/isWord.tests.js b/app/__tests__/utils/text/isWord.tests.js new file mode 100644 index 00000000..f784bcb0 --- /dev/null +++ b/app/__tests__/utils/text/isWord.tests.js @@ -0,0 +1,10 @@ +import { isWord } from '../../../lib/utils/text/isWord' + +describe(isWord.name, function () { + it('returns only true of smething is a string with length > 0', () => { + [null, undefined, false, true, '', 0, 1, {}, [], () => {}] + .forEach(val => expect(isWord(val)).toBe(false)) + ;['1', '0', ' ', '\n', '\t', 'foo'] + .forEach(val => expect(isWord(val)).toBe(true)) + }) +}) diff --git a/app/babel.config.js b/app/babel.config.js index 9d89e131..d799d652 100644 --- a/app/babel.config.js +++ b/app/babel.config.js @@ -1,6 +1,6 @@ module.exports = function (api) { - api.cache(true); + api.cache(true) return { - presets: ['babel-preset-expo'], - }; -}; + presets: ['babel-preset-expo'] + } +} diff --git a/app/build.js b/app/build.js index 1c3a939b..2d136528 100644 --- a/app/build.js +++ b/app/build.js @@ -2,7 +2,6 @@ const fs = require('node:fs') const path = require('node:path') const { spawnSync } = require('node:child_process') - let BUILD_PROFILE = 'preview' let BUILD_LOCAL = false @@ -17,7 +16,7 @@ process.argv }) const SETTINGS_PATH = path.join(process.cwd(), ((type) => { - switch(type) { + switch (type) { case 'staging': return '.deploy/settings.staging.json' case 'production': @@ -29,7 +28,7 @@ const SETTINGS_PATH = path.join(process.cwd(), ((type) => { const SETTINGS_DESTINATION = path.resolve('settings/settings.json') const ORIGINAL_SETTINGS = fs.readFileSync(SETTINGS_DESTINATION) -const settings_src = fs.readFileSync(SETTINGS_PATH) +const SETTINGS_SRC = fs.readFileSync(SETTINGS_PATH) const recoverSettings = () => { fs.writeFileSync(SETTINGS_DESTINATION, ORIGINAL_SETTINGS) @@ -38,18 +37,19 @@ const recoverSettings = () => { const attempt = (fn) => { try { fn() - } catch (e) { + } + catch (e) { console.error(e) recoverSettings() process.exit(1) } } -attempt(() => fs.writeFileSync(SETTINGS_DESTINATION, settings_src)) +attempt(() => fs.writeFileSync(SETTINGS_DESTINATION, SETTINGS_SRC)) attempt(() => { const args = ['eas', 'build', '--platform', 'android', '--profile', BUILD_PROFILE] if (BUILD_LOCAL) args.push('--local') - spawnSync('npx', args, { stdio: 'inherit' }); + spawnSync('npx', args, { stdio: 'inherit' }) recoverSettings() -}) \ No newline at end of file +}) diff --git a/app/jest.config.js b/app/jest.config.js new file mode 100644 index 00000000..7e91d135 --- /dev/null +++ b/app/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + preset: 'jest-expo', + transformIgnorePatterns: [ + 'node_modules/(?!(jest-)?react-native|@meteorrn|@react-native|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|@sentry/.*)' + ], + collectCoverage: true, + coverageDirectory: '.coverage', + coverageReporters: ['html', 'text'], + setupFiles: ['./jestSetup.js'] +} diff --git a/app/jestSetup.js b/app/jestSetup.js new file mode 100644 index 00000000..4135fea8 --- /dev/null +++ b/app/jestSetup.js @@ -0,0 +1,3 @@ +jest.mock('@react-native-async-storage/async-storage', () => + require('@react-native-async-storage/async-storage/jest/async-storage-mock') +) diff --git a/app/lib/components/BackButton.js b/app/lib/components/BackButton.js index 7b71a7bc..57fabd10 100644 --- a/app/lib/components/BackButton.js +++ b/app/lib/components/BackButton.js @@ -28,7 +28,7 @@ export const BackButton = (props) => { hitSlop={10} android_ripple={rippleConfig} > - + ) } diff --git a/app/lib/components/animated/DashedLine.js b/app/lib/components/animated/DashedLine.js index 28df1577..080fe2dd 100644 --- a/app/lib/components/animated/DashedLine.js +++ b/app/lib/components/animated/DashedLine.js @@ -9,14 +9,14 @@ export const DashedLine = props => { useEffect(() => { Animated.loop( - Animated.timing(offset, { - toValue: props.invert ? 0 : 100, - duration: 5000, - useNativeDriver: true, - isInteraction: false, - easing: Easing.linear - })).start() - }, [offset]); + Animated.timing(offset, { + toValue: props.invert ? 0 : 100, + duration: 5000, + useNativeDriver: true, + isInteraction: false, + easing: Easing.linear + })).start() + }, [offset]) return ( { - ) + ) : () } diff --git a/app/lib/contexts/MapIcons.js b/app/lib/contexts/MapIcons.js index 1c9e99a1..12753dc0 100644 --- a/app/lib/contexts/MapIcons.js +++ b/app/lib/contexts/MapIcons.js @@ -3,6 +3,8 @@ import { Colors } from '../constants/Colors' import { createContextStorage } from './createContextStorage' import Icon from '@expo/vector-icons/FontAwesome6' import { collectionNotInitialized } from './collectionNotInitialized' +import { createStyleSheet } from '../styles/createStyleSheet' +import { View } from 'react-native' export const MapIcons = { name: 'mapIcons' @@ -48,13 +50,24 @@ MapIcons.render = (index) => { const name = internal.icons[index] return ( + + ) } -const MemoIcon = React.memo(Icon) +const styles = createStyleSheet({ + container: { + alignItems: 'center', + justifyContent: 'center' + }, + icon: { + flex: 0 + } +}) diff --git a/app/lib/hooks/useConnection.js b/app/lib/hooks/useConnection.js index db4f551c..efe1f717 100644 --- a/app/lib/hooks/useConnection.js +++ b/app/lib/hooks/useConnection.js @@ -101,7 +101,7 @@ export const useConnection = () => { return { connected, www, - backend, + backend } } diff --git a/app/lib/hooks/useDevelopment.js b/app/lib/hooks/useDevelopment.js index 181ce177..1bd15c11 100644 --- a/app/lib/hooks/useDevelopment.js +++ b/app/lib/hooks/useDevelopment.js @@ -9,7 +9,7 @@ export const useDevelopment = () => { useEffect(() => { setIsDevelopment(Config.isDevelopment) setIsDeveloperRelease(Config.isDeveloperRelease()) - setIsDeveloperRelease(Config.isTest()) + setIsTest(Config.isTest()) }, []) return { isDevelopment, isDeveloperRelease, isTest } diff --git a/app/lib/i18n.js b/app/lib/i18n.js index fe4fd845..a1a7517a 100644 --- a/app/lib/i18n.js +++ b/app/lib/i18n.js @@ -25,7 +25,10 @@ const resources = { continue: 'Continue' }, connecting: { - title: 'You are offline. I\'m trying to connect.' + title: 'You are offline. I\'m trying to connect.', + done: 'You are connected again! 🎉', + backend: 'You are currently not connected to the lea-system.', + www: 'You have no internet connection, please check it.' }, actions: { back: 'Back', @@ -179,6 +182,7 @@ const resources = { continue: 'Weiter' }, connecting: { + title: 'Verbinde mit dem lea-System', done: 'Du bist wieder verbunden! 🎉', backend: 'Du bist aktuell nicht mit dem lea-System verbunden. ', www: 'Du bist aktuell nicht mit dem Internet verbunden. Bitte prüfe deine Internet\u00ADverbindung.' diff --git a/app/lib/items/cloze/createScoringSummaryForInput.js b/app/lib/items/cloze/createScoringSummaryForInput.js index be97719b..c9146e29 100644 --- a/app/lib/items/cloze/createScoringSummaryForInput.js +++ b/app/lib/items/cloze/createScoringSummaryForInput.js @@ -25,7 +25,7 @@ export const createScoringSummaryForInput = ({ itemIndex, actual, entries }) => index: itemIndex, score: 0, // computed average color: undefined, - actual: actual, + actual, entries: [] } diff --git a/app/lib/items/connect/ConnectItemRenderer.js b/app/lib/items/connect/ConnectItemRenderer.js index 87308f9e..a9e79aff 100644 --- a/app/lib/items/connect/ConnectItemRenderer.js +++ b/app/lib/items/connect/ConnectItemRenderer.js @@ -10,7 +10,6 @@ import { Svg, Circle, G } from 'react-native-svg' import { UndefinedScore } from '../../scoring/UndefinedScore' import { clearObject } from '../../utils/object/clearObject' import { ImageRenderer } from '../../components/renderer/media/ImageRenderer' -import { Layout } from '../../constants/Layout' import { Log } from '../../infrastructure/Log' import { DashedLine } from '../../components/animated/DashedLine' diff --git a/app/lib/screens/auth/RegistrationScreen.js b/app/lib/screens/auth/RegistrationScreen.js index 6ec44058..c2dc652b 100644 --- a/app/lib/screens/auth/RegistrationScreen.js +++ b/app/lib/screens/auth/RegistrationScreen.js @@ -9,7 +9,7 @@ import { createStyleSheet } from '../../styles/createStyleSheet' import { Layout } from '../../constants/Layout' import { Loading } from '../../components/Loading' import { InteractionGraph } from '../../infrastructure/log/InteractionGraph' -import { useRoute } from '@react-navigation/native'; +import { useRoute } from '@react-navigation/native' /** * Screen for registering a new user. * This screen should automatically run without further actions required. diff --git a/app/lib/screens/map/components/Milestone.js b/app/lib/screens/map/components/Milestone.js index 71bc7944..e9217e02 100644 --- a/app/lib/screens/map/components/Milestone.js +++ b/app/lib/screens/map/components/Milestone.js @@ -54,7 +54,7 @@ const getStars = (value) => { const xOffsetLeft = ((value - 1) * 9) / 2 for (let i = 0; i < value; i++) { - stars[i] = () + stars[i] = () } return stars diff --git a/app/lib/screens/map/loadMapIcons.js b/app/lib/screens/map/loadMapIcons.js deleted file mode 100644 index 4623206c..00000000 --- a/app/lib/screens/map/loadMapIcons.js +++ /dev/null @@ -1,20 +0,0 @@ -import { MapIcons } from '../../contexts/MapIcons' -import { Anchor } from './icons/Anchor' -import { Cogwheels } from './icons/Cogwheels' -import { Building } from './icons/Building' -import { Wrench } from './icons/Wrench' -import { Astronaut } from './icons/Astronaut' -import { Truck } from './icons/Truck' - -export const loadMapIcons = () => { - if (MapIcons.size() > 0) { - return false - } - MapIcons.register(Anchor) - MapIcons.register(Cogwheels) - MapIcons.register(Building) - MapIcons.register(Wrench) - MapIcons.register(Astronaut) - MapIcons.register(Truck) - return true -} diff --git a/app/lib/screens/map/loadProgressData.js b/app/lib/screens/map/loadProgressData.js index 2fec326c..59df7303 100644 --- a/app/lib/screens/map/loadProgressData.js +++ b/app/lib/screens/map/loadProgressData.js @@ -6,7 +6,6 @@ const debug = Log.create('loadProgressDoc', 'debug') export const loadProgressDoc = async (fieldId) => { debug('for', { fieldId }) - const progressDoc = await callMeteor({ name: Config.methods.getProgress, args: { fieldId }, diff --git a/app/lib/screens/unit/renderer/InstructionsGraphicsRenderer.js b/app/lib/screens/unit/renderer/InstructionsGraphicsRenderer.js index c4e56372..90c6f169 100644 --- a/app/lib/screens/unit/renderer/InstructionsGraphicsRenderer.js +++ b/app/lib/screens/unit/renderer/InstructionsGraphicsRenderer.js @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import { Vibration, View } from 'react-native' import { InstructionAnimations } from '../instructions/InstructionAnimations' import { useTts, TTSengine } from '../../../components/Tts' @@ -81,6 +81,6 @@ const styles = createStyleSheet({ flex: 1, paddingLeft: 20, paddingTop: 15, - paddingBottom: 15, + paddingBottom: 15 } }) diff --git a/app/package-lock.json b/app/package-lock.json index e8a87ce3..a4c1406a 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -50,12 +50,20 @@ }, "devDependencies": { "@babel/core": "^7.20.0", + "@testing-library/react-native": "^12.6.1", "@types/jest": "^29.5.12", "@types/react": "~18.2.45", "@types/react-test-renderer": "^18.0.7", + "eslint": "^8.57.0", + "eslint-config-expo": "^7.1.2", + "eslint-config-standard": "^17.1.0", + "eslint-plugin-jest": "^28.8.2", + "eslint-plugin-n": "^17.10.2", + "eslint-plugin-promise": "^7.1.0", "jest": "^29.2.1", "jest-expo": "~51.0.3", "react-test-renderer": "18.2.0", + "sinon": "^18.0.0", "typescript": "~5.3.3" } }, @@ -1498,6 +1506,154 @@ "node": ">=0.8.0" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/@expo/bunyan": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@expo/bunyan/-/bunyan-4.0.1.tgz", @@ -2663,6 +2819,41 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3594,6 +3785,15 @@ "node": ">= 8" } }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "engines": { + "node": ">=12.4.0" + } + }, "node_modules/@npmcli/fs": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", @@ -5773,6 +5973,12 @@ "node": ">=10" } }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true + }, "node_modules/@segment/loosely-validate-event": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@segment/loosely-validate-event/-/loosely-validate-event-2.0.0.tgz", @@ -5821,6 +6027,54 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true + }, + "node_modules/@testing-library/react-native": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-12.6.1.tgz", + "integrity": "sha512-ykXKeQEkqJ/lk0dqGfEspODjV5skmUh/6vVtyGZQsneFrcqGZJrO4uGhmChICLbnEKkstxXkjXeyoB7z8k9InQ==", + "dev": true, + "dependencies": { + "jest-matcher-utils": "^29.7.0", + "pretty-format": "^29.7.0", + "redent": "^3.0.0" + }, + "peerDependencies": { + "jest": ">=28.0.0", + "react": ">=16.8.0", + "react-native": ">=0.59", + "react-test-renderer": ">=16.8.0" + }, + "peerDependenciesMeta": { + "jest": { + "optional": true + } + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -5927,6 +6181,12 @@ "parse5": "^7.0.0" } }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, "node_modules/@types/node": { "version": "18.19.47", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.47.tgz", @@ -6007,102 +6267,338 @@ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" }, - "node_modules/@urql/core": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/@urql/core/-/core-2.3.6.tgz", - "integrity": "sha512-PUxhtBh7/8167HJK6WqBv6Z0piuiaZHQGYbhwpNL9aIQmLROPEdaUYkY4wh45wPQXcTpnd11l0q3Pw+TI11pdw==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, "dependencies": { - "@graphql-typed-document-node/core": "^3.1.0", - "wonka": "^4.0.14" + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@urql/exchange-retry": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@urql/exchange-retry/-/exchange-retry-0.3.0.tgz", - "integrity": "sha512-hHqer2mcdVC0eYnVNbWyi28AlGOPb2vjH3lP3/Bc8Lc8BjhMsDwFMm7WhoP5C1+cfbr/QJ6Er3H/L08wznXxfg==", + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, "dependencies": { - "@urql/core": ">=2.3.1", - "wonka": "^4.0.14" + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" }, - "peerDependencies": { - "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" - } - }, - "node_modules/@xmldom/xmldom": { - "version": "0.7.13", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz", - "integrity": "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==", "engines": { - "node": ">=10.0.0" + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "deprecated": "Use your platform's native atob() and btoa() methods instead", - "dev": true - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, "dependencies": { - "event-target-shim": "^5.0.0" + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" }, "engines": { - "node": ">=6.5" + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" }, "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "bin": { - "acorn": "bin/acorn" + "node": "^18.18.0 || >=20.0.0" }, - "engines": { - "node": ">=0.4.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/acorn-globals": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", - "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", "dev": true, - "dependencies": { - "acorn": "^8.1.0", - "acorn-walk": "^8.0.2" + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/acorn-walk": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", - "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", "dev": true, "dependencies": { - "acorn": "^8.11.0" + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { - "node": ">=0.4.0" + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/agent-base": { + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@urql/core": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@urql/core/-/core-2.3.6.tgz", + "integrity": "sha512-PUxhtBh7/8167HJK6WqBv6Z0piuiaZHQGYbhwpNL9aIQmLROPEdaUYkY4wh45wPQXcTpnd11l0q3Pw+TI11pdw==", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.0", + "wonka": "^4.0.14" + }, + "peerDependencies": { + "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/@urql/exchange-retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@urql/exchange-retry/-/exchange-retry-0.3.0.tgz", + "integrity": "sha512-hHqer2mcdVC0eYnVNbWyi28AlGOPb2vjH3lP3/Bc8Lc8BjhMsDwFMm7WhoP5C1+cfbr/QJ6Er3H/L08wznXxfg==", + "dependencies": { + "@urql/core": ">=2.3.1", + "wonka": "^4.0.14" + }, + "peerDependencies": { + "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz", + "integrity": "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", + "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", @@ -6273,6 +6769,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -6281,6 +6797,98 @@ "node": ">=8" } }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", @@ -7695,6 +8303,12 @@ "node": ">=4.0.0" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -7845,6 +8459,15 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -7865,10 +8488,22 @@ "node": ">=8" } }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -8007,6 +8642,19 @@ "once": "^1.4.0" } }, + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -8148,6 +8796,31 @@ "node": ">= 0.4" } }, + "node_modules/es-iterator-helpers": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", + "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-object-atoms": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", @@ -8159,52 +8832,755 @@ "node": ">= 0.4" } }, - "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "dev": true, + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-compat-utils/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-config-expo": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-expo/-/eslint-config-expo-7.1.2.tgz", + "integrity": "sha512-WxrDVNklN43Op0v3fglQfzL2bC7vqacUq9oVwJcGCUEDzdM7kGOR6pfEJiz3i3dQv3cFjHtct0CFEExep5c/dA==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "^7.4.0", + "@typescript-eslint/parser": "^7.4.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-expo": "^0.0.1", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-react": "^7.34.0", + "eslint-plugin-react-hooks": "^4.6.0" + }, + "peerDependencies": { + "eslint": ">=8.10" + } + }, + "node_modules/eslint-config-standard": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", + "integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": "^8.0.1", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", + "eslint-plugin-promise": "^6.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.3.tgz", + "integrity": "sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA==", + "dev": true, + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.3.5", + "enhanced-resolve": "^5.15.0", + "eslint-module-utils": "^2.8.1", + "fast-glob": "^3.3.2", + "get-tsconfig": "^4.7.5", + "is-bun-module": "^1.0.2", + "is-glob": "^4.0.3" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.9.0.tgz", + "integrity": "sha512-McVbYmwA3NEKwRQY5g4aWMdcZE5xZxV8i8l7CqJSrameuGSQJtSWaL/LxTEzSKKaCcOhlpDR8XEfYXWPrdo/ZQ==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-es-x": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz", + "integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/ota-meshi", + "https://opencollective.com/eslint" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.1.2", + "@eslint-community/regexpp": "^4.11.0", + "eslint-compat-utils": "^0.5.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": ">=8" + } + }, + "node_modules/eslint-plugin-expo": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-expo/-/eslint-plugin-expo-0.0.1.tgz", + "integrity": "sha512-dNri81vunJ3T+N1YWWxjLU6ux6KiukwZ4ECXCOPp8hG7M4kuvPAb9YQSIM63AT0pbtfYH/a6htikhaQcRPjhRA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "^7.2.0", + "@typescript-eslint/utils": "^7.2.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "eslint": ">=8" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz", + "integrity": "sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==", + "dev": true, + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.9.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "28.8.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.8.2.tgz", + "integrity": "sha512-mC3OyklHmS5i7wYU1rGId9EnxRI8TVlnFG56AE+8U9iRy6zwaNygZR+DsdZuCL0gRG0wVeyzq+uWcPt6yJrrMA==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "engines": { + "node": "^16.10.0 || ^18.12.0 || >=20.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-n": { + "version": "17.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.10.2.tgz", + "integrity": "sha512-e+s4eAf5NtJaxPhTNu3qMO0Iz40WANS93w9LQgYcvuljgvDmWi/a3rh+OrNyMHeng6aOWGJO0rCg5lH4zi8yTw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "enhanced-resolve": "^5.17.0", + "eslint-plugin-es-x": "^7.5.0", + "get-tsconfig": "^4.7.0", + "globals": "^15.8.0", + "ignore": "^5.2.4", + "minimatch": "^9.0.5", + "semver": "^7.5.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": ">=8.23.0" + } + }, + "node_modules/eslint-plugin-n/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/eslint-plugin-n/node_modules/globals": { + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz", + "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-n/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-plugin-n/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-promise": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-7.1.0.tgz", + "integrity": "sha512-8trNmPxdAy3W620WKDpaS65NlM5yAumod6XeC4LOb+jxlkG4IVcp68c6dXY2ev+uT4U1PtG57YDV6EGAXN0GbQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.35.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.1.tgz", + "integrity": "sha512-B5ok2JgbaaWn/zXbKCGgKDNL2tsID3Pd/c/yvjcpsd9HQDwyYc/TQv3AZMmOvrJgCs3AnYNUHRCQEMMQAYJ7Yg==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.19", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "dependencies": { - "get-intrinsic": "^1.2.4", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 0.4" + "node": ">=10.13.0" } }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "type-fest": "^0.20.2" }, "engines": { - "node": ">= 0.4" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, "engines": { "node": ">=10" }, @@ -8212,35 +9588,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": ">=6.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/escodegen/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/esprima": { @@ -8255,6 +9617,30 @@ "node": ">=4" } }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", @@ -8794,6 +10180,12 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, "node_modules/fast-loops": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.4.tgz", @@ -8873,6 +10265,18 @@ "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-4.1.1.tgz", "integrity": "sha512-e6eB7zN6UBSwGVwrbWVH+gdLnkW9WwHhmq2YDK1Sh30pzx1onRVGBvogTlUeWxwTa+L86NYdo4hFkh7O8ZjSnA==" }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -9045,6 +10449,26 @@ "micromatch": "^4.0.2" } }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, "node_modules/flow-enums-runtime": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz", @@ -9279,6 +10703,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.0.tgz", + "integrity": "sha512-Pgba6TExTZ0FJAn1qkJAjIeKoDJ3CsI2ChuLohJnZl/tTU8MVrq3b+2t5UOPfRa4RMsorClBjJALkJUMjG1PAw==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/getenv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/getenv/-/getenv-1.0.0.tgz", @@ -9376,6 +10812,12 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "node_modules/graphql": { "version": "15.8.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz", @@ -9847,6 +11289,21 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-bigint": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", @@ -9878,6 +11335,27 @@ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, + "node_modules/is-bun-module": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.1.0.tgz", + "integrity": "sha512-4mTAVPlrXpaN3jtF0lsnPCMGnq4+qZjVIKq0HCpfcqf8OC1SM5oATCIAPM5V5FN05qp2NNnFndphmdZS9CV3hA==", + "dev": true, + "dependencies": { + "semver": "^7.6.3" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -9961,6 +11439,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", @@ -9978,6 +11468,21 @@ "node": ">=6" } }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -10027,6 +11532,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-negative-zero": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", @@ -10116,6 +11633,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-shared-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", @@ -10202,6 +11731,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -10213,6 +11754,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-weakset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -10350,6 +11907,19 @@ "node": ">=8" } }, + "node_modules/iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -12515,6 +14085,12 @@ "node": ">=4" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -12559,6 +14135,12 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -12578,6 +14160,36 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -12602,6 +14214,19 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lighthouse-logger": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", @@ -12838,11 +14463,23 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, "node_modules/lodash.throttle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", @@ -13662,6 +15299,15 @@ "node": ">=4" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -13868,6 +15514,28 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, + "node_modules/nise": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.0.0.tgz", + "integrity": "sha512-K8ePqo9BFvN31HXwEtTNGzgrPpmvgciDsFz8aztFjt4LqKO/JeFD8tBOeuDiCMXrIl/m1YvfH8auSpxfaD09wg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.3.1.tgz", + "integrity": "sha512-EVJO7nW5M/F5Tur0Rf2z/QoMo+1Ia963RiMtapiQrEWvY0iBUvADo8Beegwjpnle5BHkyHuoxSTW3jF43H1XRA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, "node_modules/nocache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/nocache/-/nocache-3.0.4.tgz", @@ -14083,6 +15751,69 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -14145,6 +15876,23 @@ "opencollective-postinstall": "index.js" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ora": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz", @@ -14269,6 +16017,18 @@ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -14376,6 +16136,12 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, + "node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "dev": true + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -14557,6 +16323,15 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -15338,6 +17113,40 @@ "node": ">=0.10.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", + "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -15500,6 +17309,15 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -15913,6 +17731,54 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, + "node_modules/sinon": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.0.tgz", + "integrity": "sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^6.0.0", + "supports-color": "^7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/@sinonjs/fake-timers": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.3.1.tgz", + "integrity": "sha512-EVJO7nW5M/F5Tur0Rf2z/QoMo+1Ia963RiMtapiQrEWvY0iBUvADo8Beegwjpnle5BHkyHuoxSTW3jF43H1XRA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/sinon/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -16210,6 +18076,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -16304,6 +18180,18 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -16444,6 +18332,15 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -16771,16 +18668,73 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/tslib": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -17061,6 +19015,15 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/url-join": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.0.tgz", @@ -17284,6 +19247,56 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-builtin-type": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz", + "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==", + "dev": true, + "dependencies": { + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/which-module": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", @@ -17312,6 +19325,15 @@ "resolved": "https://registry.npmjs.org/wonka/-/wonka-4.0.15.tgz", "integrity": "sha512-U0IUQHKXXn6PFo9nqsHphVCE5m3IntqZNB9Jjn7EB1lrR7YTDY3YWgFvEvwniTzXSvOH/XMzAZaIfJF/LvHYXg==" }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/app/package.json b/app/package.json index a85f5367..c549271f 100644 --- a/app/package.json +++ b/app/package.json @@ -8,14 +8,12 @@ "android": "expo start --android", "ios": "expo start --ios", "web": "expo start --web", - "test": "jest --watchAll", + "test": "jest --coverage", + "test:fails": "jest --watch --bail", "lint": "expo lint", "build:staging": "node build --type=staging", "build:prod": "node build --type=production" }, - "jest": { - "preset": "jest-expo" - }, "dependencies": { "@expo/vector-icons": "^14.0.2", "@meteorrn/core": "^2.8.2-rc.1", @@ -59,12 +57,20 @@ }, "devDependencies": { "@babel/core": "^7.20.0", + "@testing-library/react-native": "^12.6.1", "@types/jest": "^29.5.12", "@types/react": "~18.2.45", "@types/react-test-renderer": "^18.0.7", + "eslint": "^8.57.0", + "eslint-config-expo": "^7.1.2", + "eslint-config-standard": "^17.1.0", + "eslint-plugin-jest": "^28.8.2", + "eslint-plugin-n": "^17.10.2", + "eslint-plugin-promise": "^7.1.0", "jest": "^29.2.1", "jest-expo": "~51.0.3", "react-test-renderer": "18.2.0", + "sinon": "^18.0.0", "typescript": "~5.3.3" }, "private": true diff --git a/app/settings/settings.json b/app/settings/settings.json index a763f7da..6df651b7 100644 --- a/app/settings/settings.json +++ b/app/settings/settings.json @@ -1,12 +1,12 @@ { - "isDevelopment": false, - "isDeveloperRelease": false, + "isDevelopment": true, + "isDeveloperRelease": true, "appToken": "01234567890123456789012345678901", "backend": { - "url": "wss://appbackend.lealernen.de/websocket", - "reachabilityUrl": "https://appbackend.lealernen.de/reachability", - "maxTimeout": 100000, - "interval": 500, + "url": "ws://192.168.178.46:8080/websocket", + "reachabilityUrl": "http://192.168.178.46:8080/reachability", + "maxTimeout": 3000, + "interval": 250, "methods": { "defaultTimeout": 5000, "sendError": "clientErrors.methods.send", @@ -46,7 +46,7 @@ } }, "content": { - "url": "https://content.lealernen.de" + "url": "http://192.168.178.22:3030" }, "log": { "level": 0, @@ -71,4 +71,4 @@ "AppSession": false, "connection": false } -} +} \ No newline at end of file From 4c407da975bd3514c0d2279b24d39342bb3e1c93 Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Tue, 3 Sep 2024 11:48:06 +0200 Subject: [PATCH 2/4] docs: updated backend/app docs --- app/jsdoc.conf.json | 26 + app/lib/hooks/useRefresh.js | 2 +- app/package-lock.json | 205 +++++ app/package.json | 2 + docs/api/app/App.js.html | 32 +- docs/api/app/AuthenticationError.html | 4 +- docs/api/app/ChoiceImageInstructions.html | 228 +++++ docs/api/app/ClozeRenderer.html | 4 +- docs/api/app/ConnectItemRenderer.html | 6 +- docs/api/app/ConnectionError.html | 4 +- docs/api/app/ImageRenderer.html | 6 +- docs/api/app/LeaCollection.html | 4 +- docs/api/app/Loading.html | 286 ++++++ docs/api/app/MeteorError_MeteorError.html | 4 +- docs/api/app/SyncScreen.html | 4 +- docs/api/app/UnitContentElementFactory.html | 4 +- docs/api/app/components_ActionButton.js.html | 4 +- .../api/app/components_CharacterInput.js.html | 4 +- docs/api/app/components_Checkbox.js.html | 4 +- docs/api/app/components_Confirm.js.html | 8 +- docs/api/app/components_Connecting.js.html | 69 +- docs/api/app/components_ErrorMessage.js.html | 38 +- docs/api/app/components_FadePanel.js.html | 4 +- docs/api/app/components_LeaButton.js.html | 10 +- .../api/app/components_LeaButtonGroup.js.html | 4 +- docs/api/app/components_LeaText.js.html | 4 +- docs/api/app/components_Loading.js.html | 107 +++ .../app/components_MarkdownWithTTS.js.html | 4 +- docs/api/app/components_NullComponent.js.html | 4 +- docs/api/app/components_RouteButton.js.html | 4 +- docs/api/app/components_SoundIcon.js.html | 4 +- docs/api/app/components_Tts.js.html | 43 +- ...actories_UnitContentElementFactory.js.html | 4 +- ..._factories_createRoutableComponent.js.html | 4 +- .../api/app/components_images_LeaLogo.js.html | 6 +- ...omponents_progress_CurrentProgress.js.html | 4 +- .../app/components_progress_Diamond.js.html | 4 +- ...ts_progress_StaticCircularProgress.js.html | 4 +- ...ts_progress_correctDiamondProgress.js.html | 4 +- ...nents_renderer_media_ImageRenderer.js.html | 35 +- .../components_renderer_text_Markdown.js.html | 4 +- ...ts_renderer_text_PlainTextRenderer.js.html | 4 +- docs/api/app/constants_Colors.js.html | 4 +- docs/api/app/constants_Layout.js.html | 37 +- docs/api/app/contexts_UserProgress.js.html | 4 +- .../contexts_collectionNotInitialized.js.html | 4 +- docs/api/app/dev_DeveloperScreen.js.html | 4 +- docs/api/app/dev_loadDevData.js.html | 4 +- docs/api/app/dev_resetSyncData.js.html | 4 +- docs/api/app/env_Config.js.html | 7 +- .../env_loadSettingsFromUserProfile.js.html | 4 +- .../app/errors_AuthenticationError.js.html | 4 +- docs/api/app/errors_ConnectionError.js.html | 4 +- docs/api/app/errors_MeteorError.js.html | 4 +- docs/api/app/global.html | 814 +++++++++++++++++- docs/api/app/hooks_useBackHandler.js.html | 4 +- docs/api/app/hooks_useConnection.js.html | 156 ++-- docs/api/app/hooks_useLogin.js.html | 65 +- docs/api/app/hooks_useRefresh.js.html | 66 ++ docs/api/app/index.html | 12 +- docs/api/app/infrastructure_Log.js.html | 4 +- ...tructure_collections_LeaCollection.js.html | 4 +- ...astructure_collections_collections.js.html | 4 +- .../infrastructure_createCollection.js.html | 4 +- .../infrastructure_createRepository.js.html | 4 +- ...nfrastructure_log_InteractionGraph.js.html | 4 +- docs/api/app/infrastructure_sync_Sync.js.html | 4 +- .../app/items_choice_ChoiceRenderer.js.html | 4 +- ...ms_choice_getChoiceEntryScoreColor.js.html | 4 +- docs/api/app/items_cloze_Cloze.js.html | 4 +- .../api/app/items_cloze_ClozeRenderer.js.html | 4 +- .../items_cloze_ClozeRendererBlank.js.html | 4 +- .../items_cloze_ClozeRendererSelect.js.html | 4 +- .../app/items_cloze_ClozeTokenizer.js.html | 4 +- ...cloze_createScoringSummaryForInput.js.html | 6 +- .../items_connect_ConnectItemRenderer.js.html | 194 +++-- ...getCompareValuesForSelectableItems.js.html | 4 +- docs/api/app/items_utils_CompareState.js.html | 4 +- .../api/app/items_utils_KeyboardTypes.js.html | 4 +- docs/api/app/meteor_call.js.html | 35 +- docs/api/app/meteor_ensureConnected.js.html | 4 +- docs/api/app/meteor_updateUserProfile.js.html | 4 +- docs/api/app/meteor_useDocs.js.html | 100 ++- .../api/app/navigation_MainNavigation.js.html | 25 +- docs/api/app/remotes_ContentServer.js.html | 4 +- docs/api/app/schema_createSchema.js.html | 4 +- .../app/schema_validateSettingsSchema.js.html | 6 +- docs/api/app/scoring_Scoring.js.html | 4 +- docs/api/app/scoring_ScoringTypes.js.html | 4 +- docs/api/app/scoring_getScoring.js.html | 4 +- .../app/scoring_isUndefinedResponse.js.html | 4 +- docs/api/app/screens_BaseScreen.js.html | 51 +- .../screens_auth_RegistrationScreen.js.html | 18 +- ...eens_auth_TermsAndConditionsScreen.js.html | 10 +- .../app/screens_auth_WelcomeScreen.js.html | 4 +- .../screens_complete_CompleteScreen.js.html | 9 +- docs/api/app/screens_home_HomeScreen.js.html | 24 +- .../api/app/screens_home_loadHomeData.js.html | 75 ++ .../app/screens_map_DimensionScreen.js.html | 4 +- docs/api/app/screens_map_MapScreen.js.html | 143 +-- .../screens_map_components_Connector.js.html | 103 ++- .../screens_map_components_Milestone.js.html | 25 +- .../app/screens_map_components_Stage.js.html | 4 +- docs/api/app/screens_map_loadMapData.js.html | 18 +- .../app/screens_profile_ProfileScreen.js.html | 25 +- ...creens_profile_account_AccountInfo.js.html | 52 +- ...le_achievements_AchievementsScreen.js.html | 4 +- ..._achievements_loadAchievementsData.js.html | 4 +- docs/api/app/screens_sync_SyncScreen.js.html | 4 +- docs/api/app/screens_unit_UnitScreen.js.html | 12 +- .../api/app/screens_unit_completeUnit.js.html | 4 +- .../screens_unit_createResponseDoc.js.html | 4 +- .../screens_unit_getDimensionColor.js.html | 4 +- ...structions_ChoiceImageInstructions.js.html | 206 +++++ ...eens_unit_renderer_ContentRenderer.js.html | 4 +- ...derer_InstructionsGraphicsRenderer.js.html | 25 +- ...screens_unit_renderer_UnitRenderer.js.html | 102 ++- ...eens_unit_renderer_UnitSetRenderer.js.html | 4 +- .../screens_unit_shouldRenderStory.js.html | 4 +- docs/api/app/state_AppSession.js.html | 4 +- docs/api/app/state_createStorageAPI.js.html | 4 +- docs/api/app/styles_createStyleSheet.js.html | 4 +- docs/api/app/tts_TTSVoiceConfig.js.html | 78 +- docs/api/app/utils_array_byDocId.js.html | 4 +- docs/api/app/utils_array_byOrderedIds.js.html | 4 +- .../utils_array_randomArrayElement.js.html | 4 +- docs/api/app/utils_array_toArrayIfNot.js.html | 4 +- docs/api/app/utils_array_toDocId.js.html | 4 +- docs/api/app/utils_asyncTimeout.js.html | 4 +- docs/api/app/utils_createTimedPromise.js.html | 6 +- .../app/utils_math_randomIntInclusive.js.html | 4 +- .../app/utils_number_isSafeInteger.js.html | 4 +- .../app/utils_number_isValidNumber.js.html | 4 +- docs/api/app/utils_number_toInteger.js.html | 4 +- .../utils_number_toPrecisionNumber.js.html | 4 +- docs/api/app/utils_object_clearObject.js.html | 4 +- docs/api/app/utils_object_hasOwnProp.js.html | 4 +- docs/api/app/utils_object_isDefined.js.html | 4 +- docs/api/app/utils_simpleRandomHex.js.html | 4 +- .../utils_text_createSimpleTokenizer.js.html | 4 +- docs/api/app/utils_text_isWord.js.html | 4 +- ...s_trigonometry_getPositionOnCircle.js.html | 4 +- 142 files changed, 3183 insertions(+), 807 deletions(-) create mode 100644 app/jsdoc.conf.json create mode 100644 docs/api/app/ChoiceImageInstructions.html create mode 100644 docs/api/app/Loading.html create mode 100644 docs/api/app/components_Loading.js.html create mode 100644 docs/api/app/hooks_useRefresh.js.html create mode 100644 docs/api/app/screens_home_loadHomeData.js.html create mode 100644 docs/api/app/screens_unit_instructions_ChoiceImageInstructions.js.html diff --git a/app/jsdoc.conf.json b/app/jsdoc.conf.json new file mode 100644 index 00000000..431f0df4 --- /dev/null +++ b/app/jsdoc.conf.json @@ -0,0 +1,26 @@ +{ + "tags": { + "allowUnknownTags": true, + "dictionaries": ["jsdoc", "closure"] + }, + "source": { + "include": ["."], + "exclude": [ + ".expo", + ".expo-shared", + "__mocks__", + "__tests__", + "node_modules" + ], + "includePattern": ".+\\.js(doc|x)?$", + "excludePattern": "(^|\\/|\\\\)_" + }, + "plugins": [ + "plugins/markdown" + ], + "opts": { + "destination": "../docs/api/app", + "recurse": true, + "readme": "../README.md" + } +} diff --git a/app/lib/hooks/useRefresh.js b/app/lib/hooks/useRefresh.js index 13061b6b..5e4149a1 100644 --- a/app/lib/hooks/useRefresh.js +++ b/app/lib/hooks/useRefresh.js @@ -4,7 +4,7 @@ import { useCallback, useState } from 'react' * A little helper hook, that can be used to mediate between * {useDocs} and {BaseScreen} in order to implement a * page-refresh functionality. - * @return {[number,(function(): void)|*]} + * @return {Array} */ export const useRefresh = () => { const [reload, setReload] = useState(0) diff --git a/app/package-lock.json b/app/package-lock.json index a4c1406a..c8537607 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -62,6 +62,7 @@ "eslint-plugin-promise": "^7.1.0", "jest": "^29.2.1", "jest-expo": "~51.0.3", + "jsdoc": "^4.0.3", "react-test-renderer": "18.2.0", "sinon": "^18.0.0", "typescript": "~5.3.3" @@ -3722,6 +3723,18 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdoc/salty": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.8.tgz", + "integrity": "sha512-5e+SFVavj1ORKlKaKr2BmTOekmXbelU7dC0cDkQLqag7xfuTPuGMUFx7KWJuv4bYZrTsoL2Z18VVCOKYxzoHcg==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v12.0.0" + } + }, "node_modules/@meteorrn/core": { "version": "2.8.2-rc.1", "resolved": "https://registry.npmjs.org/@meteorrn/core/-/core-2.8.2-rc.1.tgz", @@ -6187,6 +6200,28 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true + }, "node_modules/@types/node": { "version": "18.19.47", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.47.tgz", @@ -7291,6 +7326,12 @@ "node": ">= 6" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -7594,6 +7635,18 @@ } ] }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "dev": true, + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -13909,6 +13962,15 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dev": true, + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, "node_modules/jsc-android": { "version": "250231.0.0", "resolved": "https://registry.npmjs.org/jsc-android/-/jsc-android-250231.0.0.tgz", @@ -14015,6 +14077,100 @@ "node": ">=8" } }, + "node_modules/jsdoc": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.3.tgz", + "integrity": "sha512-Nu7Sf35kXJ1MWDZIMAuATRQTg1iIPdzh7tqJ6jjvaU/GfDf+qi5UV8zJR3Mo+/pYFvm8mzay4+6O5EWigaQBQw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^14.1.1", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^8.6.7", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdoc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jsdoc/node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/jsdoc/node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/jsdoc/node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true + }, + "node_modules/jsdoc/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jsdoc/node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true + }, "node_modules/jsdom": { "version": "20.0.3", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", @@ -14198,6 +14354,15 @@ "node": ">=0.10.0" } }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -14717,6 +14882,16 @@ "markdown-it": "bin/markdown-it.js" } }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "dev": true, + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, "node_modules/markdown-it/node_modules/entities": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", @@ -16438,6 +16613,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -17273,6 +17457,15 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -18896,6 +19089,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "dev": true + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -19525,6 +19724,12 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "dev": true + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/app/package.json b/app/package.json index c549271f..db4dda9d 100644 --- a/app/package.json +++ b/app/package.json @@ -11,6 +11,7 @@ "test": "jest --coverage", "test:fails": "jest --watch --bail", "lint": "expo lint", + "build:docs": "jsdoc -c jsdoc.conf.json", "build:staging": "node build --type=staging", "build:prod": "node build --type=production" }, @@ -69,6 +70,7 @@ "eslint-plugin-promise": "^7.1.0", "jest": "^29.2.1", "jest-expo": "~51.0.3", + "jsdoc": "^4.0.3", "react-test-renderer": "18.2.0", "sinon": "^18.0.0", "typescript": "~5.3.3" diff --git a/docs/api/app/App.js.html b/docs/api/app/App.js.html index d81829d2..f26eec8e 100644 --- a/docs/api/app/App.js.html +++ b/docs/api/app/App.js.html @@ -26,7 +26,7 @@

Source: App.js

-
import React, { useCallback } from 'react'
+            
import React, { useCallback, useEffect, useState } from 'react'
 import './i18n'
 import { useSplashScreen } from './hooks/useSplashScreen'
 import { useConnection } from './hooks/useConnection'
@@ -40,10 +40,8 @@ 

Source: App.js

import { CatchErrors } from './components/CatchErrors' import { initSound } from './startup/initSound' import { validateSettingsSchema } from './schema/validateSettingsSchema' -import { initFonts } from './startup/initFonts' const initFunctions = [ - initFonts, initExceptionHandling, validateSettingsSchema, initContexts, @@ -59,13 +57,23 @@

Source: App.js

*/ export const App = function App () { const { appIsReady, error, onLayoutRootView } = useSplashScreen(initFunctions) - const { connected } = useConnection() - const renderConnectionStatus = useCallback(() => { - if (connected) { - return null + const connection = useConnection() + const [showWarning, setShowWarning] = useState(false) + + useEffect(() => { + if (showWarning && connection.connected) { + setShowWarning(false) + } + if (!showWarning && !connection.connected) { + setShowWarning(true) } - return (<Connecting />) - }, [connected]) + }, [showWarning, connection.connected]) + + const renderConnectionStatus = useCallback(() => { + return showWarning + ? (<Connecting connection={connection} />) + : null + }, [showWarning, connection.www, connection.backend]) // splashscreen is still active... if (!appIsReady) { return null } @@ -82,7 +90,7 @@

Source: App.js

return ( <CatchErrors> - <MainNavigation onLayout={onLayoutRootView} /> + <MainNavigation onLayout={onLayoutRootView} connection={connection} /> {renderConnectionStatus()} </CatchErrors> ) @@ -97,13 +105,13 @@

Source: App.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/AuthenticationError.html b/docs/api/app/AuthenticationError.html index ab1943d4..a8746d0c 100644 --- a/docs/api/app/AuthenticationError.html +++ b/docs/api/app/AuthenticationError.html @@ -272,13 +272,13 @@

Classes


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/ChoiceImageInstructions.html b/docs/api/app/ChoiceImageInstructions.html new file mode 100644 index 00000000..9d74ff86 --- /dev/null +++ b/docs/api/app/ChoiceImageInstructions.html @@ -0,0 +1,228 @@ + + + + + JSDoc: Class: ChoiceImageInstructions + + + + + + + + + + +
+ +

Class: ChoiceImageInstructions

+ + + + + + +
+ +
+ +

ChoiceImageInstructions(props) → {Element}

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

new ChoiceImageInstructions(props) → {Element}

+ + + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
props + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Element + + +
+
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time) +
+ + + + + \ No newline at end of file diff --git a/docs/api/app/ClozeRenderer.html b/docs/api/app/ClozeRenderer.html index 0e79570b..a253cdae 100644 --- a/docs/api/app/ClozeRenderer.html +++ b/docs/api/app/ClozeRenderer.html @@ -336,13 +336,13 @@
Returns:

- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/ConnectItemRenderer.html b/docs/api/app/ConnectItemRenderer.html index a69ec43a..c72c68af 100644 --- a/docs/api/app/ConnectItemRenderer.html +++ b/docs/api/app/ConnectItemRenderer.html @@ -147,7 +147,7 @@
Parameters:
Source:
@@ -227,13 +227,13 @@
Returns:

- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/ConnectionError.html b/docs/api/app/ConnectionError.html index 1e83405a..a979988b 100644 --- a/docs/api/app/ConnectionError.html +++ b/docs/api/app/ConnectionError.html @@ -278,13 +278,13 @@

Classes


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/ImageRenderer.html b/docs/api/app/ImageRenderer.html index 68fd093c..cc79e1de 100644 --- a/docs/api/app/ImageRenderer.html +++ b/docs/api/app/ImageRenderer.html @@ -268,7 +268,7 @@
Properties
Source:
@@ -351,13 +351,13 @@
Returns:

- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/LeaCollection.html b/docs/api/app/LeaCollection.html index 4ef1c114..697d25e3 100644 --- a/docs/api/app/LeaCollection.html +++ b/docs/api/app/LeaCollection.html @@ -318,13 +318,13 @@
Returns:

- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/Loading.html b/docs/api/app/Loading.html new file mode 100644 index 00000000..2aa7d526 --- /dev/null +++ b/docs/api/app/Loading.html @@ -0,0 +1,286 @@ + + + + + JSDoc: Class: Loading + + + + + + + + + + +
+ +

Class: Loading

+ + + + + + +
+ +
+ +

Loading(text, color, style, timeOut) → {Element}

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

new Loading(text, color, style, timeOut) → {Element}

+ + + + + + +
+

Renders a ActivityIndicator with an optional message.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
text + +
color + +
style + +
timeOut + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Element + + +
+
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time) +
+ + + + + \ No newline at end of file diff --git a/docs/api/app/MeteorError_MeteorError.html b/docs/api/app/MeteorError_MeteorError.html index d08be771..c0e35665 100644 --- a/docs/api/app/MeteorError_MeteorError.html +++ b/docs/api/app/MeteorError_MeteorError.html @@ -249,13 +249,13 @@
Parameters:

- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/SyncScreen.html b/docs/api/app/SyncScreen.html index a38c4201..be6d3a74 100644 --- a/docs/api/app/SyncScreen.html +++ b/docs/api/app/SyncScreen.html @@ -246,13 +246,13 @@
Returns:

- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/UnitContentElementFactory.html b/docs/api/app/UnitContentElementFactory.html index 03c2a5b2..070c7e98 100644 --- a/docs/api/app/UnitContentElementFactory.html +++ b/docs/api/app/UnitContentElementFactory.html @@ -853,13 +853,13 @@
Parameters:

- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_ActionButton.js.html b/docs/api/app/components_ActionButton.js.html index 29f7a8af..092ca051 100644 --- a/docs/api/app/components_ActionButton.js.html +++ b/docs/api/app/components_ActionButton.js.html @@ -108,13 +108,13 @@

Source: components/ActionButton.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_CharacterInput.js.html b/docs/api/app/components_CharacterInput.js.html index b971a46b..bc9a05fc 100644 --- a/docs/api/app/components_CharacterInput.js.html +++ b/docs/api/app/components_CharacterInput.js.html @@ -229,13 +229,13 @@

Source: components/CharacterInput.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_Checkbox.js.html b/docs/api/app/components_Checkbox.js.html index e2765bbf..4bed58fd 100644 --- a/docs/api/app/components_Checkbox.js.html +++ b/docs/api/app/components_Checkbox.js.html @@ -147,13 +147,13 @@

Source: components/Checkbox.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_Confirm.js.html b/docs/api/app/components_Confirm.js.html index 7780fe75..fb213ba5 100644 --- a/docs/api/app/components_Confirm.js.html +++ b/docs/api/app/components_Confirm.js.html @@ -32,7 +32,7 @@

Source: components/Confirm.js

import { createStyleSheet } from '../styles/createStyleSheet' import { useTts } from './Tts' import { Colors } from '../constants/Colors' -import { Icon } from 'react-native-elements' +import Icon from '@expo/vector-icons/FontAwesome6' import { makeTransparent } from '../styles/makeTransparent' import { Layout } from '../constants/Layout' @@ -89,7 +89,7 @@

Source: components/Confirm.js

return ( <Pressable accessibilityRole='button' style={styles.buttonContainer} onPress={onPress}> <Icon - name={props.icon} type='font-awesome-5' color={Colors.secondary} + name={props.icon} color={Colors.secondary} style size={18} /> @@ -208,13 +208,13 @@

Source: components/Confirm.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_Connecting.js.html b/docs/api/app/components_Connecting.js.html index e65db247..d6d2dfbf 100644 --- a/docs/api/app/components_Connecting.js.html +++ b/docs/api/app/components_Connecting.js.html @@ -26,35 +26,67 @@

Source: components/Connecting.js

-
import React from 'react'
-import { ActivityIndicator, Modal, View } from 'react-native'
+            
import React, { useState } from 'react'
+import { Modal, View } from 'react-native'
 import { createStyleSheet } from '../styles/createStyleSheet'
 import { Layout } from '../constants/Layout'
 import { useTranslation } from 'react-i18next'
 import { useTts } from './Tts'
 import { makeTransparent } from '../styles/makeTransparent'
 import { Colors } from '../constants/Colors'
+import Icon from '@expo/vector-icons/FontAwesome'
+import { useDevelopment } from '../hooks/useDevelopment'
+
+const iconSize = 46
+const minusSize = 20
 
 /**
  * Default visual for indicating to users, that we are connecting to the servers.
  * @returns {JSX.Element}
  * @component
  */
-export const Connecting = () => {
+export const Connecting = ({ connection }) => {
   const { t } = useTranslation()
-  const { Tts } = useTts()
+  const [visible, setVisible] = useState(true)
+  const dev = useDevelopment()
+  const reachBackend = connection.www && connection.backend
+  let description
+
+  if (reachBackend) {
+    description = t('connecting.done')
+  }
+  else if (connection.www) {
+    description = t('connecting.backend')
+  }
+  else {
+    description = t('connecting.www')
+  }
+
+  const close = () => {
+    if (!dev.isDeveloperRelease && !dev.isDevelopment) {
+      return
+    }
+    setVisible(!visible)
+  }
 
   return (
     <Modal
       animationType='slide'
       transparent
-      visible
+      visible={visible}
+      onRequestClose={close}
     >
       <View style={styles.background}>
         <View style={styles.content}>
           <View style={styles.container}>
-            <ActivityIndicator style={styles.indicator} color={Colors.primary} size='large' />
-            <Tts text={t('connecting.title')} />
+            <View style={styles.row}>
+              <Icon name='mobile' size={iconSize} color={Colors.success} />
+              <Connection connected={connection.www} available />
+              <Icon name='wifi' size={iconSize} color={connection.www ? Colors.success : Colors.danger} />
+              <Connection connected={reachBackend} available={connection.www} />
+              <Icon name='cloud' size={iconSize} color={reachBackend ? Colors.success : Colors.danger} />
+            </View>
+            <Description text={description} />
           </View>
         </View>
       </View>
@@ -62,6 +94,21 @@ 

Source: components/Connecting.js

) } +const Description = ({ text }) => { + const { Tts } = useTts() + return (<Tts text={text} />) +} + +const Connection = ({ connected }) => { + const arrowColor = connected ? Colors.success : Colors.danger + + return ( + <View style={styles.row}> + <Icon name='minus' size={minusSize} color={arrowColor} /> + </View> + ) +} + const styles = createStyleSheet({ background: { flexGrow: 1, @@ -80,6 +127,10 @@

Source: components/Connecting.js

alignItems: 'center', ...Layout.dropShadow({ elevation: 6 }) }, + row: { + ...Layout.row(), + padding: 2 + }, indicator: { height: 50 } @@ -94,13 +145,13 @@

Source: components/Connecting.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_ErrorMessage.js.html b/docs/api/app/components_ErrorMessage.js.html index 71cafd1e..07faf821 100644 --- a/docs/api/app/components_ErrorMessage.js.html +++ b/docs/api/app/components_ErrorMessage.js.html @@ -26,10 +26,10 @@

Source: components/ErrorMessage.js

-
import React from 'react'
+            
import React, { useEffect } from 'react'
 import { useTts } from './Tts'
 import { ActionButton } from './ActionButton'
-import { Image, Text, View } from 'react-native'
+import { Image, Text, Vibration, View } from 'react-native'
 import { createStyleSheet } from '../styles/createStyleSheet'
 import { Colors } from '../constants/Colors'
 import { Layout } from '../constants/Layout'
@@ -53,6 +53,10 @@ 

Source: components/ErrorMessage.js

const { t } = useTranslation() const { Tts } = useTts() + useEffect(() => { + Vibration.vibrate(100) + }, []) + if (!error && !message) { return null } @@ -71,8 +75,6 @@

Source: components/ErrorMessage.js

} const debugError = () => { - // if (!Config.isDevelopment) { return null } - return ( <View style={styles.container} accessibilityRole='alert'> <Text>Debugging Info</Text> @@ -89,6 +91,13 @@

Source: components/ErrorMessage.js

return ( <View style={styles.container} accessibilityRole='alert'> + <Tts + text={textBase} + fontStyle={styles.headline} + block + iconColor={Colors.danger} + color={Colors.secondary} + /> <Image source={image.src} style={styles.image} @@ -96,12 +105,6 @@

Source: components/ErrorMessage.js

resizeMethod='resize' resizeMode='contain' /> - <Tts - text={textBase} - block - iconColor={Colors.danger} - color={Colors.secondary} - /> <Tts text={t('errors.restart')} block @@ -115,7 +118,7 @@

Source: components/ErrorMessage.js

} const image = { - src: require('../assets/images/sorry.png') + src: require('../../assets/images/sorry.png') } /** @private */ @@ -130,8 +133,13 @@

Source: components/ErrorMessage.js

...Layout.dropShadow() }, image: { - flex: 1, - width: '100%' + shrink: 1, + width: '100%', + marginTop: 10, + marginBottom: 10 + }, + headline: { + fontWeight: 'bold' } })
@@ -144,13 +152,13 @@

Source: components/ErrorMessage.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_FadePanel.js.html b/docs/api/app/components_FadePanel.js.html index a110379c..0eea2f26 100644 --- a/docs/api/app/components_FadePanel.js.html +++ b/docs/api/app/components_FadePanel.js.html @@ -82,13 +82,13 @@

Source: components/FadePanel.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_LeaButton.js.html b/docs/api/app/components_LeaButton.js.html index d899711b..79fb3471 100644 --- a/docs/api/app/components_LeaButton.js.html +++ b/docs/api/app/components_LeaButton.js.html @@ -26,7 +26,8 @@

Source: components/LeaButton.js

-
import { Button, Icon } from 'react-native-elements'
+            
import { Button } from 'react-native-elements'
+import Icon from '@expo/vector-icons/FontAwesome6'
 import React, { useState } from 'react'
 import { createStyleSheet } from '../styles/createStyleSheet'
 import { Colors } from '../constants/Colors'
@@ -98,7 +99,7 @@ 

Source: components/LeaButton.js

setTimeout(async () => { await props.onPress() setPressed(false) - }, 25) + }, 50) } } @@ -139,7 +140,6 @@

Source: components/LeaButton.js

id: 'icon-id', color: Colors.secondary, size: 18, - type: 'font-awesome-5', position: 'left' } } @@ -182,13 +182,13 @@

Source: components/LeaButton.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_LeaButtonGroup.js.html b/docs/api/app/components_LeaButtonGroup.js.html index f8143716..f7964ad2 100644 --- a/docs/api/app/components_LeaButtonGroup.js.html +++ b/docs/api/app/components_LeaButtonGroup.js.html @@ -118,13 +118,13 @@

Source: components/LeaButtonGroup.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_LeaText.js.html b/docs/api/app/components_LeaText.js.html index 38454db6..c510786b 100644 --- a/docs/api/app/components_LeaText.js.html +++ b/docs/api/app/components_LeaText.js.html @@ -95,13 +95,13 @@

Source: components/LeaText.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_Loading.js.html b/docs/api/app/components_Loading.js.html new file mode 100644 index 00000000..33809c37 --- /dev/null +++ b/docs/api/app/components_Loading.js.html @@ -0,0 +1,107 @@ + + + + + JSDoc: Source: components/Loading.js + + + + + + + + + + +
+ +

Source: components/Loading.js

+ + + + + + +
+
+
import React, { useCallback, useEffect, useState } from 'react'
+import { View, ActivityIndicator } from 'react-native'
+import { useTts } from './Tts'
+import { Colors } from '../constants/Colors'
+import { createStyleSheet } from '../styles/createStyleSheet'
+import { mergeStyles } from '../styles/mergeStyles'
+
+/**
+ * Renders a ActivityIndicator with an optional message.
+ * @param text
+ * @param color
+ * @param style
+ * @param timeOut
+ * @return {Element}
+ * @constructor
+ */
+export const Loading = ({ text, color, style, timeOut = 700 }) => {
+  const [showText, setShowText] = useState(false)
+  const { Tts } = useTts()
+  const renderText = useCallback(() => {
+    if (!text || !showText) return null
+    return (
+      <Tts text={text} />
+    )
+  }, [text, showText])
+
+  useEffect(() => {
+    let timer
+    if (timeOut > 0) {
+      timer = setTimeout(() => setShowText(true), timeOut)
+    }
+    else {
+      setShowText(true)
+    }
+    // prevent memleak
+    return () => clearTimeout(timer)
+  }, [timeOut])
+
+  const containerStyle = style
+    ? mergeStyles(styles.container, style)
+    : styles.container
+
+  return (
+    <View style={containerStyle}>
+      <ActivityIndicator size='large' color={color ?? Colors.secondary} />
+      {renderText()}
+    </View>
+  )
+}
+
+const styles = createStyleSheet({
+  container: {
+    alignItems: 'center',
+    justifyContent: 'center'
+  }
+})
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time) +
+ + + + + diff --git a/docs/api/app/components_MarkdownWithTTS.js.html b/docs/api/app/components_MarkdownWithTTS.js.html index f14221f3..ceab8727 100644 --- a/docs/api/app/components_MarkdownWithTTS.js.html +++ b/docs/api/app/components_MarkdownWithTTS.js.html @@ -150,13 +150,13 @@

Source: components/MarkdownWithTTS.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_NullComponent.js.html b/docs/api/app/components_NullComponent.js.html index e4ada92e..73c9a879 100644 --- a/docs/api/app/components_NullComponent.js.html +++ b/docs/api/app/components_NullComponent.js.html @@ -42,13 +42,13 @@

Source: components/NullComponent.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_RouteButton.js.html b/docs/api/app/components_RouteButton.js.html index 1eccc478..3926e971 100644 --- a/docs/api/app/components_RouteButton.js.html +++ b/docs/api/app/components_RouteButton.js.html @@ -85,13 +85,13 @@

Source: components/RouteButton.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_SoundIcon.js.html b/docs/api/app/components_SoundIcon.js.html index 31ad2340..39c9cb20 100644 --- a/docs/api/app/components_SoundIcon.js.html +++ b/docs/api/app/components_SoundIcon.js.html @@ -125,13 +125,13 @@

Source: components/SoundIcon.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_Tts.js.html b/docs/api/app/components_Tts.js.html index 6d33c442..40ca6189 100644 --- a/docs/api/app/components_Tts.js.html +++ b/docs/api/app/components_Tts.js.html @@ -69,26 +69,28 @@

Source: components/Tts.js

* Tts stands for Text-To-Speech. It contains an icon and the text to be spoken. * * @category Components - * @param {string} props.text: The displayed and spoken text - * @param {string} props.ttsText: The spoken text, use this if it differs from written text - * @param {boolean} props.dontShowText: Determines whether the text is displayed (Default 'true') - * @param {boolean} props.smallButton: Changes the button size from 20 to 15 (Default 'false') - * @param {boolean=} props.block: Makes the container flexGrow. If this causes problems, use style instead. - * @param {boolean=} props.asButton: Makes the container a block-sized button - * @param {boolean=} props.disabled: Makes the button disabled - * @param {string=} props.color: The color of the icon and the text, in hexadecimal format. Default: Colors.secondary + * @param props {object} + * @param props.text {string} The displayed and spoken text + * @param props.ttsText {string} The spoken text, use this if it differs from written text + * @param props.dontShowText {boolean} Determines whether the text is displayed (Default 'true') + * @param props.smallButton {boolean} Changes the button size from 20 to 15 (Default 'false') + * @param props.block {boolean=} Makes the container flexGrow. If this causes problems, use style instead. + * @param props.asButton {boolean=} Makes the container a block-sized button + * @param props.disabled {boolean=} Makes the button disabled + * @param props.color {string=} The color of the icon and the text, in hexadecimal format. Default: Colors.secondary * (examples in ./constants/Colors.js) - * @param {string=} props.iconColor: The color of the icon in hexadecimal format. Default: Colors.secondary (examples + * @param props.iconColor {string=} The color of the icon in hexadecimal format. Default: Colors.secondary (examples * in ./constants/Colors.js) - * @param {string=} props.activeIconColor: The color of the icon when speaking is active - * @param {number} props.shrink: The parameter to shrink the text. Default: 1 - * @param {number} props.fontSize: The parameter to change the font size of the text. Default: 18 - * @param {string} props.fontStyle: The parameter to change the font style of the text. Default: 'normal' ('italic') - * @param {object=} props.style: The parameter to change the font style of the text. Default: 'normal' ('italic') - * @param {string} props.align Defines the vertical alignment of the button and text - * @param {number} props.paddingTop: Determines the top padding of the text. Default: 8 - * @param {number} props.speed: Determines the speed rate of the voice to speak. Default: 1.0 - * @param {string|number} props.id: The parameter to identify the buttons + * @param props.activeIconColor {string=} The color of the icon when speaking is active + * @param props.shrink {number} The parameter to shrink the text. Default: 1 + * @param props.fontSize {number} The parameter to change the font size of the text. Default: 18 + * @param props.fontStyle {string} The parameter to change the font style of the text. Default: 'normal' ('italic') + * @param props.style {object=} The parameter to change the font style of the text. Default: 'normal' ('italic') + * @param props.align {string} Defines the vertical alignment of the button and text + * @param props.paddingTop {number} Determines the top padding of the text. Default: 8 + * @param props.speed {number} Determines the speed rate of the voice to speak. Default: 1.0 + * @param props.buttonRef {Component?} optional ref passed to connect to button + * @param props.id {string|number} The parameter to identify the buttons * @returns {JSX.Element} * @component */ @@ -285,6 +287,7 @@

Source: components/Tts.js

} return ( <Button + ref={props.buttonRef} accessibilityRole='button' testID={props.testId} containerStyle={ttsContainerStyle} @@ -507,13 +510,13 @@

Source: components/Tts.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_factories_UnitContentElementFactory.js.html b/docs/api/app/components_factories_UnitContentElementFactory.js.html index de4402c9..86fb9bc0 100644 --- a/docs/api/app/components_factories_UnitContentElementFactory.js.html +++ b/docs/api/app/components_factories_UnitContentElementFactory.js.html @@ -117,13 +117,13 @@

Source: components/factories/UnitContentElementFactory.js
- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_factories_createRoutableComponent.js.html b/docs/api/app/components_factories_createRoutableComponent.js.html index c7822e1a..84d66327 100644 --- a/docs/api/app/components_factories_createRoutableComponent.js.html +++ b/docs/api/app/components_factories_createRoutableComponent.js.html @@ -75,13 +75,13 @@

Source: components/factories/createRoutableComponent.js
- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_images_LeaLogo.js.html b/docs/api/app/components_images_LeaLogo.js.html index 404cc68e..997c24a0 100644 --- a/docs/api/app/components_images_LeaLogo.js.html +++ b/docs/api/app/components_images_LeaLogo.js.html @@ -32,7 +32,7 @@

Source: components/images/LeaLogo.js

const logos = { footer: { - src: require('../../assets/logo-footer.png'), + src: require('../../../assets/logo-footer.png'), styles: {} } } @@ -61,13 +61,13 @@

Source: components/images/LeaLogo.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_progress_CurrentProgress.js.html b/docs/api/app/components_progress_CurrentProgress.js.html index 73e5c6c2..76df93a7 100644 --- a/docs/api/app/components_progress_CurrentProgress.js.html +++ b/docs/api/app/components_progress_CurrentProgress.js.html @@ -96,13 +96,13 @@

Source: components/progress/CurrentProgress.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_progress_Diamond.js.html b/docs/api/app/components_progress_Diamond.js.html index 907b0704..ec608409 100644 --- a/docs/api/app/components_progress_Diamond.js.html +++ b/docs/api/app/components_progress_Diamond.js.html @@ -120,13 +120,13 @@

Source: components/progress/Diamond.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_progress_StaticCircularProgress.js.html b/docs/api/app/components_progress_StaticCircularProgress.js.html index 590768ba..7fb88d04 100644 --- a/docs/api/app/components_progress_StaticCircularProgress.js.html +++ b/docs/api/app/components_progress_StaticCircularProgress.js.html @@ -152,13 +152,13 @@

Source: components/progress/StaticCircularProgress.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_progress_correctDiamondProgress.js.html b/docs/api/app/components_progress_correctDiamondProgress.js.html index d1f0d024..5d345b85 100644 --- a/docs/api/app/components_progress_correctDiamondProgress.js.html +++ b/docs/api/app/components_progress_correctDiamondProgress.js.html @@ -56,13 +56,13 @@

Source: components/progress/correctDiamondProgress.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_renderer_media_ImageRenderer.js.html b/docs/api/app/components_renderer_media_ImageRenderer.js.html index 2db5f53f..b04de688 100644 --- a/docs/api/app/components_renderer_media_ImageRenderer.js.html +++ b/docs/api/app/components_renderer_media_ImageRenderer.js.html @@ -33,6 +33,10 @@

Source: components/renderer/media/ImageRenderer.js

import { Loading } from '../../Loading' import { ContentServer } from '../../../remotes/ContentServer' import { mergeStyles } from '../../../styles/mergeStyles' +import { Colors } from '../../../constants/Colors' +import Icon from '@expo/vector-icons/FontAwesome6' +import { useTts } from '../../Tts' +import { useTranslation } from 'react-i18next' const win = Dimensions.get('window') const debug = Log.create('ImageRenderer', 'debug', true) @@ -47,6 +51,8 @@

Source: components/renderer/media/ImageRenderer.js

* @constructor */ export const ImageRenderer = props => { + const { Tts } = useTts() + const { t } = useTranslation() const widthRatio = props.width ? Number.parseInt(props.width) / 12 : 1 @@ -60,10 +66,12 @@

Source: components/renderer/media/ImageRenderer.js

Log.error(error) setLoadComplete(true) setError(error) + if (props.onError) { props.onError(error) } }, onLoadEnd: event => { debug('load end from', urlReplaced) setTimeout(() => setLoadComplete(true), 300) + if (props.onLoaded) { props.onLoaded() } }, // other potential events: // onLayout: event => console.debug('layout', event.nativeEvent), @@ -72,10 +80,6 @@

Source: components/renderer/media/ImageRenderer.js

resizeMethod: 'auto' } - if (error) { - return null - } - const loader = () => loadComplete ? null : (<Loading />) @@ -83,7 +87,15 @@

Source: components/renderer/media/ImageRenderer.js

return ( <View style={styles.imageContainer}> {loader()} - <Image {...imageProps} accessibilityRole='image' resizeMode='center' /> + {error + ? ( + <View style={styles.fallback}> + <Tts text={t('errors.imageFailed')} color={Colors.gray} dontShowText /> + <Icon name="image" size={48} color={Colors.gray} /> + </View> + ) + : (<Image {...imageProps} accessibilityRole='image' resizeMode='center' />) + } </View> ) } @@ -96,6 +108,15 @@

Source: components/renderer/media/ImageRenderer.js

imageContainer: { flexDirection: 'row', alignItems: 'flex-start' + }, + fallback: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + minWidth: 100, + minHeight: 100, + borderWidth: 1, + borderColor: Colors.gray } })
@@ -108,13 +129,13 @@

Source: components/renderer/media/ImageRenderer.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_renderer_text_Markdown.js.html b/docs/api/app/components_renderer_text_Markdown.js.html index fba3cdba..d50cb5de 100644 --- a/docs/api/app/components_renderer_text_Markdown.js.html +++ b/docs/api/app/components_renderer_text_Markdown.js.html @@ -68,13 +68,13 @@

Source: components/renderer/text/Markdown.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/components_renderer_text_PlainTextRenderer.js.html b/docs/api/app/components_renderer_text_PlainTextRenderer.js.html index 0caf3b33..736586bd 100644 --- a/docs/api/app/components_renderer_text_PlainTextRenderer.js.html +++ b/docs/api/app/components_renderer_text_PlainTextRenderer.js.html @@ -60,13 +60,13 @@

Source: components/renderer/text/PlainTextRenderer.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/constants_Colors.js.html b/docs/api/app/constants_Colors.js.html index 0b5f43cb..52483683 100644 --- a/docs/api/app/constants_Colors.js.html +++ b/docs/api/app/constants_Colors.js.html @@ -57,13 +57,13 @@

Source: constants/Colors.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/constants_Layout.js.html b/docs/api/app/constants_Layout.js.html index 92c285d8..0cee7db4 100644 --- a/docs/api/app/constants_Layout.js.html +++ b/docs/api/app/constants_Layout.js.html @@ -29,7 +29,6 @@

Source: constants/Layout.js

import { Colors } from './Colors'
 import Constants from 'expo-constants'
 import { Dimensions, PixelRatio } from 'react-native'
-import { fontIsLoaded } from '../utils/fontIsLoaded'
 
 const window = Dimensions.get('window')
 const screen = Dimensions.get('screen')
@@ -76,6 +75,16 @@ 

Source: constants/Layout.js

justifyContent: 'space-around' }) +Layout.row = () => { + return { + flexDirection: 'row', + // alignItems: 'stretch', + // justifyItems: 'stretch', + justifyContent: 'space-around', + alignItems: 'center' + } +} + Layout.content = () => { return { padding: 20 @@ -87,7 +96,7 @@

Source: constants/Layout.js

* components. */ Layout.input = () => { - const style = { + return { padding: 5, fontSize: Layout.fontSize() / Layout.fontScale(), color: Colors.secondary, @@ -96,32 +105,22 @@

Source: constants/Layout.js

borderTopLeftRadius: 4, borderTopRightRadius: 4, borderBottomRightRadius: 4, - borderBottomLeftRadius: 4 - } - - if (fontIsLoaded(SEMICOLON_FONT)) { - style.fontFamily = SEMICOLON_FONT + borderBottomLeftRadius: 4, + fontFamily: SEMICOLON_FONT } - - return style } Layout.defaultFont = () => { - const style = { + return { color: Colors.secondary, fontSize: Layout.fontSize() / Layout.fontScale(), lineHeight: 28, fontStyle: 'normal', fontWeight: 'normal', padding: 0, - margin: 0 + margin: 0, + fontFamily: SEMICOLON_FONT } - - if (fontIsLoaded(SEMICOLON_FONT)) { - style.fontFamily = SEMICOLON_FONT - } - - return style } Layout.borderRadius = () => 15 @@ -164,13 +163,13 @@

Source: constants/Layout.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/contexts_UserProgress.js.html b/docs/api/app/contexts_UserProgress.js.html index 04b66f7f..8128c8db 100644 --- a/docs/api/app/contexts_UserProgress.js.html +++ b/docs/api/app/contexts_UserProgress.js.html @@ -159,13 +159,13 @@

Source: contexts/UserProgress.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/contexts_collectionNotInitialized.js.html b/docs/api/app/contexts_collectionNotInitialized.js.html index 29339500..739efd0d 100644 --- a/docs/api/app/contexts_collectionNotInitialized.js.html +++ b/docs/api/app/contexts_collectionNotInitialized.js.html @@ -45,13 +45,13 @@

Source: contexts/collectionNotInitialized.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/dev_DeveloperScreen.js.html b/docs/api/app/dev_DeveloperScreen.js.html index bf1042f9..94fd04f5 100644 --- a/docs/api/app/dev_DeveloperScreen.js.html +++ b/docs/api/app/dev_DeveloperScreen.js.html @@ -286,13 +286,13 @@

Source: dev/DeveloperScreen.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/dev_loadDevData.js.html b/docs/api/app/dev_loadDevData.js.html index cd06afb3..82f5a052 100644 --- a/docs/api/app/dev_loadDevData.js.html +++ b/docs/api/app/dev_loadDevData.js.html @@ -99,13 +99,13 @@

Source: dev/loadDevData.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/dev_resetSyncData.js.html b/docs/api/app/dev_resetSyncData.js.html index c9d31e0f..c83044bc 100644 --- a/docs/api/app/dev_resetSyncData.js.html +++ b/docs/api/app/dev_resetSyncData.js.html @@ -54,13 +54,13 @@

Source: dev/resetSyncData.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/env_Config.js.html b/docs/api/app/env_Config.js.html index 46ca34d8..17b89383 100644 --- a/docs/api/app/env_Config.js.html +++ b/docs/api/app/env_Config.js.html @@ -27,7 +27,7 @@

Source: env/Config.js

/* global __DEV__ */
-import settings from '../settings.json'
+import settings from '../../settings/settings.json' // check metro.config.js
 
 const { appToken, backend, content, log, debug, isDevelopment, isDeveloperRelease } = settings
 
@@ -57,6 +57,7 @@ 

Source: env/Config.js

* This is useful, if you want to debug positioning, padding and margin or flex layout. */ Config.debug.layoutBorders = debug.layoutBorders +Config.debug.connection = debug.connection Config.log = {} Config.log.level = log.level @@ -137,13 +138,13 @@

Source: env/Config.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/env_loadSettingsFromUserProfile.js.html b/docs/api/app/env_loadSettingsFromUserProfile.js.html index f8c9d4c8..ca506c8e 100644 --- a/docs/api/app/env_loadSettingsFromUserProfile.js.html +++ b/docs/api/app/env_loadSettingsFromUserProfile.js.html @@ -69,13 +69,13 @@

Source: env/loadSettingsFromUserProfile.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/errors_AuthenticationError.js.html b/docs/api/app/errors_AuthenticationError.js.html index d6c7a5a8..0a163831 100644 --- a/docs/api/app/errors_AuthenticationError.js.html +++ b/docs/api/app/errors_AuthenticationError.js.html @@ -59,13 +59,13 @@

Source: errors/AuthenticationError.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/errors_ConnectionError.js.html b/docs/api/app/errors_ConnectionError.js.html index 3880d3fa..2be89cb6 100644 --- a/docs/api/app/errors_ConnectionError.js.html +++ b/docs/api/app/errors_ConnectionError.js.html @@ -65,13 +65,13 @@

Source: errors/ConnectionError.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/errors_MeteorError.js.html b/docs/api/app/errors_MeteorError.js.html index 16477589..4835bc5b 100644 --- a/docs/api/app/errors_MeteorError.js.html +++ b/docs/api/app/errors_MeteorError.js.html @@ -100,13 +100,13 @@

Source: errors/MeteorError.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/global.html b/docs/api/app/global.html index b6187abd..9578f9a2 100644 --- a/docs/api/app/global.html +++ b/docs/api/app/global.html @@ -336,7 +336,7 @@

(constant) AppSource:
@@ -1113,7 +1113,7 @@

(constant)
Source:
@@ -1624,7 +1624,7 @@

(constant)
Source:
@@ -1765,7 +1765,7 @@

(constant) L
Source:
@@ -2012,7 +2012,7 @@

(constant) M
Source:
@@ -2328,7 +2328,7 @@

(constant) Source:
@@ -2591,7 +2591,7 @@
Type:
Source:
@@ -2781,7 +2781,7 @@

(constant) Source:
@@ -3035,7 +3035,7 @@
Properties:
Source:
@@ -3098,7 +3098,7 @@

(con
Source:
@@ -3165,7 +3165,7 @@

(constant) Source:
@@ -5599,6 +5599,68 @@

(constant) (constant) loadHomeData

+ + + + +
+

Loads the available fields for the home screen.

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + +

(constant) loadMapData

@@ -6465,7 +6527,7 @@

(constant) Source:
@@ -6597,7 +6659,7 @@

(constant) use
Source:
@@ -6691,6 +6753,70 @@

(constant) us +

(constant) useRefresh

+ + + + +
+

A little helper hook, that can be used to mediate between +{useDocs} and {BaseScreen} in order to implement a +page-refresh functionality.

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + +

(constant) validateSettingsSchema

@@ -7872,7 +7998,7 @@
Properties
Source:
@@ -8552,6 +8678,39 @@
Properties
+ + + + + + + + onRefresh + + + + + +function + + + + + + + + + + + <nullable>
+ + + + + + + + @@ -8600,7 +8759,7 @@
Properties
Source:
@@ -8654,7 +8813,7 @@
Returns:
-

TtsComponent() → {JSX.Element}

+

TtsComponent(props) → {JSX.Element}

@@ -8686,6 +8845,48 @@
Parameters:
Type + + + + Description + + + + + + + + + props + + + + + +object + + + + + + + + + + +
Properties
+ + + + + + + + + + + + @@ -8700,7 +8901,7 @@
Parameters:
- + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + +
NameTypeAttributes
props.text:text @@ -8731,7 +8932,7 @@
Parameters:
props.ttsText:ttsText @@ -8762,7 +8963,7 @@
Parameters:
props.dontShowText:dontShowText @@ -8793,7 +8994,7 @@
Parameters:
props.smallButton:smallButton @@ -8824,7 +9025,7 @@
Parameters:
props.block:block @@ -8857,7 +9058,7 @@
Parameters:
props.asButton:asButton @@ -8890,7 +9091,7 @@
Parameters:
props.disabled:disabled @@ -8923,7 +9124,7 @@
Parameters:
props.color:color @@ -8957,7 +9158,7 @@
Parameters:
props.iconColor:iconColor @@ -8991,7 +9192,7 @@
Parameters:
props.activeIconColor:activeIconColor @@ -9024,7 +9225,7 @@
Parameters:
props.shrink:shrink @@ -9055,7 +9256,7 @@
Parameters:
props.fontSize:fontSize @@ -9086,7 +9287,7 @@
Parameters:
props.fontStyle:fontStyle @@ -9117,7 +9318,7 @@
Parameters:
props.style:style @@ -9150,7 +9351,7 @@
Parameters:
props.alignalign @@ -9181,7 +9382,7 @@
Parameters:
props.paddingTop:paddingTop @@ -9212,7 +9413,7 @@
Parameters:
props.speed:speed @@ -9243,7 +9444,40 @@
Parameters:
props.id:buttonRef + + +Component + + + + + + + + <nullable>
+ + + +

optional ref passed to connect to button

id @@ -9277,6 +9511,13 @@
Parameters:
+ + + + + + + @@ -9311,7 +9552,7 @@
Parameters:
Source:
@@ -9501,7 +9742,7 @@
Parameters:
Source:
@@ -9690,7 +9931,7 @@
Parameters:
Source:
@@ -9720,6 +9961,501 @@
Parameters:
+ + + + + + +

signUp(termsAndConditionsIsCheckednullable, voicenullable, speednullable, onError, onSuccess) → {Promise.<void>}

+ + + + + + +
+

Registers a new user account on the backend server

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
termsAndConditionsIsChecked + + +boolean + + + + + + + + <nullable>
+ + + +
voice + + +string + + + + + + + + <nullable>
+ + + +

identifier of the selected voice

speed + + +number + + + + + + + + <nullable>
+ + + +

speed value for voice

onError + + +function + + + + + + + + + +
onSuccess + + +function + + + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Promise.<void> + + +
+
+ + + + + + + + + + + + + +

usePath(from, to, width, height) → {string}

+ + + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
from + +
to + +
width + +
height + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + @@ -9736,13 +10472,13 @@
Parameters:

- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/hooks_useBackHandler.js.html b/docs/api/app/hooks_useBackHandler.js.html index 40ed632a..67d59aff 100644 --- a/docs/api/app/hooks_useBackHandler.js.html +++ b/docs/api/app/hooks_useBackHandler.js.html @@ -52,13 +52,13 @@

Source: hooks/useBackHandler.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/hooks_useConnection.js.html b/docs/api/app/hooks_useConnection.js.html index 6cd94685..4296808d 100644 --- a/docs/api/app/hooks_useConnection.js.html +++ b/docs/api/app/hooks_useConnection.js.html @@ -26,70 +26,122 @@

Source: hooks/useConnection.js

-
import { useState } from 'react'
+            
import { useEffect, useState } from 'react'
 import Meteor from '@meteorrn/core'
 import * as SecureStore from 'expo-secure-store'
 import { Config } from '../env/Config'
 import { Log } from '../infrastructure/Log'
-
-const log = Log.create('useConnection')
-
-const connect = (() => {
-  let started = false
-
-  return () => {
-    if (started) {
-      return false
-    }
-    else {
-      started = true
-    }
-
-    // get detailed info about internals
-    Meteor.enableVerbose()
-
-    log('connect Meteor to backend at', Config.backend.url)
-    log('url for reachability test is', Config.backend.reachabilityUrl)
-
-    // connect with Meteor and use a secure store
-    // to persist our received login token, so it's encrypted
-    // and only readable for this very app
-    // read more at: https://docs.expo.dev/versions/latest/sdk/securestore/
-    Meteor.connect(Config.backend.url, {
-      AsyncStorage: {
-        getItem: SecureStore.getItemAsync,
-        setItem: SecureStore.setItemAsync,
-        removeItem: SecureStore.deleteItemAsync
-      },
-      autoConnect: true,
-      autoReconnect: true,
-      reconnectInterval: 500,
-      reachabilityUrl: Config.backend.reachabilityUrl
-    })
-
-    return false
-  }
-})()
+import { AppState } from 'react-native'
+import { useNetInfo } from '@react-native-community/netinfo'
+
+Meteor.enableVerbose()
+
+// connect with Meteor and use a secure store
+// to persist our received login token, so it's encrypted
+// and only readable for this very app
+// read more at: https://docs.expo.dev/versions/latest/sdk/securestore/
+Meteor.connect(Config.backend.url, {
+  AsyncStorage: {
+    getItem: SecureStore.getItemAsync,
+    setItem: SecureStore.setItemAsync,
+    removeItem: SecureStore.deleteItemAsync
+  },
+  autoConnect: false,
+  autoReconnect: true,
+  reconnectInterval: 3000,
+  NetInfo: null
+})
 
 /**
  * Hook that handle auto-reconnect and updates state accordingly.
  * @return {{connected: boolean|null}}
  */
 export const useConnection = () => {
-  const [connected, setConnected] = useState(() => connect())
-  const status = Meteor.useTracker(() => Meteor.status())
+  const appState = useAppState()
+  const [connected, setConnected] = useState(null)
+  const [status, setStatus] = useState('connecting')
+  const { isConnected } = useNetInfo({
+    reachabilityUrl: Config.backend.reachabilityUrl,
+    reachabilityTest: async (response) => response.status === 204,
+    reachabilityLongTimeout: 15 * 1000, // 15s
+    reachabilityShortTimeout: 5 * 1000, // 5s
+    reachabilityRequestTimeout: 15 * 1000, // 15s
+    reachabilityShouldRun: () => appState === 'active' && !connected,
+    shouldFetchWiFiSSID: true, // met iOS requirements to get SSID
+    useNativeReachability: false
+  })
+
+  const www = !!isConnected
+  const backend = status === 'connected'
+
+  useEffect(() => {
+    const Data = Meteor.getData()
+    const updateConnected = (backendConnected) => {
+      if (backendConnected && connected !== true) {
+        log('set connected=true')
+        setConnected(true)
+      }
+
+      if (connected !== false && !backendConnected) {
+        log('set connected=false')
+        setConnected(false)
+      }
+    }
+    Data.ddp.on('error', e => {
+      // Log.error(e)
+      log('DDP on error')
+      Log.info(e.message)
+    })
 
-  if (status.connected && !connected) {
-    log(Config.backend.url, 'connected', status)
-    setConnected(true)
-  }
+    Data.ddp.on('connected', () => {
+      log('DDP on connected')
+      setStatus('connected')
+      updateConnected(true)
+    })
+
+    Data.ddp.on('disconnected', () => {
+      log('DDP on disconnected')
+      setStatus('connecting')
+      updateConnected(false)
+    })
+
+    Meteor.reconnect()
 
-  if (connected && !status.connected) {
-    log(Config.backend.url, 'disconnected', status)
-    setConnected(false)
+    return () => {
+      Data.ddp.off('error')
+      Data.ddp.off('connected')
+      Data.ddp.off('disconnected')
+    }
+  }, [])
+
+  useEffect(() => {
+    if ((isConnected && appState === 'active') && connected === false) {
+      log('connect to server')
+      Meteor.getData().ddp.connect()
+    }
+
+    if ((!isConnected || appState === 'background') && connected === true) {
+      log('disconnect from server')
+      Meteor.getData().ddp.disconnect()
+    }
+  }, [appState, connected, isConnected])
+
+  return {
+    connected,
+    www,
+    backend
   }
+}
+
+const log = Log.create('useConnection')
 
-  return { connected }
+const useAppState = () => {
+  const [appState, setAppState] = useState(AppState.currentState)
+  useEffect(() => {
+    const listener = AppState.addEventListener('change', newState => setAppState(newState))
+    return () => listener.remove()
+  }, [])
+  return appState
 }
 
@@ -101,13 +153,13 @@

Source: hooks/useConnection.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/hooks_useLogin.js.html b/docs/api/app/hooks_useLogin.js.html index 8780d4fb..ebcac72a 100644 --- a/docs/api/app/hooks_useLogin.js.html +++ b/docs/api/app/hooks_useLogin.js.html @@ -29,7 +29,6 @@

Source: hooks/useLogin.js

import { useReducer, useEffect, useMemo } from 'react'
 import Meteor from '@meteorrn/core'
 import { loadSettingsFromUserProfile } from '../env/loadSettingsFromUserProfile'
-import { useConnection } from './useConnection'
 import { Config } from '../env/Config'
 import { getDeviceData } from '../analystics/getDeviceData'
 import { Log } from '../infrastructure/Log'
@@ -39,6 +38,10 @@ 

Source: hooks/useLogin.js

const initialState = { isLoading: true, isSignout: false, + /** + * we will shortly remove this + * @deprecated + */ isDeleted: false, userToken: null } @@ -82,9 +85,6 @@

Source: hooks/useLogin.js

} } -/** @private */ -const Data = Meteor.getData() - /** * Provides a state and authentication context for components to decide, whether * the user is authenticated and also to run several authentication actions. @@ -109,19 +109,22 @@

Source: hooks/useLogin.js

* authContext: object * }} */ -export const useLogin = () => { - const { connected } = useConnection() +export const useLogin = ({ connection }) => { + const { connected } = connection const [state, dispatch] = useReducer(reducer, initialState, undefined) + const user = Meteor.useTracker(() => Meteor.user()) - Meteor.useTracker(() => { - if (!connected) { return } - const user = Meteor.user() + useEffect(() => { + if (!connected || !user || state.profileLoaded) { return } - if (!state.profileLoaded && user) { + try { loadSettingsFromUserProfile(user) - dispatch({ type: 'PROFILE_LOADED' }) } - }, [connected]) + catch (e) { + ErrorReporter.send({ error: e }).catch(Log.error) + } + dispatch({ type: 'PROFILE_LOADED' }) + }, [connected, user]) // Case 1: restore token already exists // MeteorRN loads the token on connection automatically, @@ -144,8 +147,8 @@

Source: hooks/useLogin.js

// Note, that 'onLogin' is only fired, when the // package has successfully restored the login via token! else { - Data.on('onLogin', handleOnLogin) - return () => Data.off('onLogin', handleOnLogin) + Meteor.getData().on('onLogin', handleOnLogin) + return () => Meteor.getData().off('onLogin', handleOnLogin) } }, [connected]) @@ -162,8 +165,18 @@

Source: hooks/useLogin.js

dispatch({ type: 'SIGN_OUT' }) }) }, - signUp: async ({ voice, speed, onError, onSuccess }) => { - const args = { voice, speed } + + /** + * Registers a new user account on the backend server + * @param termsAndConditionsIsChecked {boolean?} + * @param voice {string?} identifier of the selected voice + * @param speed {number?} speed value for voice + * @param onError {function} + * @param onSuccess {function} + * @return {Promise<void>} + */ + signUp: async ({ termsAndConditionsIsChecked, voice, speed, onError, onSuccess }) => { + const args = { voice, speed, termsAndConditionsIsChecked } if (Config.isDeveloperRelease()) { args.isDev = true @@ -227,21 +240,29 @@

Source: hooks/useLogin.js

}) }, deleteAccount: ({ onError, onSuccess }) => { - Meteor.call(Config.methods.deleteUser, {}, (deleteError) => { + Meteor.call(Config.methods.deleteUser, {}, async (deleteError) => { if (deleteError) { return onError(deleteError) } // instead of calling Meteor.logout we // directly invoke the logout handler + if (onSuccess) { + try { + await onSuccess() + } + catch (e) { + onError(e) + } + } + Meteor.handleLogout() - onSuccess && onSuccess() - dispatch({ type: 'DELETE' }) + dispatch({ type: 'SIGN_OUT' }) }) } }), []) - return { state, authContext } + return { state, user, authContext } }

@@ -253,13 +274,13 @@

Source: hooks/useLogin.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/hooks_useRefresh.js.html b/docs/api/app/hooks_useRefresh.js.html new file mode 100644 index 00000000..2afd0559 --- /dev/null +++ b/docs/api/app/hooks_useRefresh.js.html @@ -0,0 +1,66 @@ + + + + + JSDoc: Source: hooks/useRefresh.js + + + + + + + + + + +
+ +

Source: hooks/useRefresh.js

+ + + + + + +
+
+
import { useCallback, useState } from 'react'
+
+/**
+ * A little helper hook, that can be used to mediate between
+ * {useDocs} and {BaseScreen} in order to implement a
+ * page-refresh functionality.
+ * @return {Array<number, function(): void>}
+ */
+export const useRefresh = () => {
+  const [reload, setReload] = useState(0)
+  const refresh = useCallback(() => {
+    setReload(reload + 1)
+  }, [reload])
+  return [reload, refresh]
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time) +
+ + + + + diff --git a/docs/api/app/index.html b/docs/api/app/index.html index 04a0b60a..1097e172 100644 --- a/docs/api/app/index.html +++ b/docs/api/app/index.html @@ -44,11 +44,11 @@

lea.online App

-

Build Android APK -Test suite +

Test suite Lint Test -Project Status: WIP – Initial development is in progress, but there has not yet been a stable, usable release suitable for the public. -GitHub

+Project Status: Active – The project has reached a stable, usable state and is being actively developed. +GitHub +DOI

About

The lea.online app is a mobile app, developed using React-Native and Meteor.

Get the app

@@ -85,13 +85,13 @@

License


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/infrastructure_Log.js.html b/docs/api/app/infrastructure_Log.js.html index 84e7cdd2..01a69ac4 100644 --- a/docs/api/app/infrastructure_Log.js.html +++ b/docs/api/app/infrastructure_Log.js.html @@ -186,13 +186,13 @@

Source: infrastructure/Log.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/infrastructure_collections_LeaCollection.js.html b/docs/api/app/infrastructure_collections_LeaCollection.js.html index 7245fd54..fac9c630 100644 --- a/docs/api/app/infrastructure_collections_LeaCollection.js.html +++ b/docs/api/app/infrastructure_collections_LeaCollection.js.html @@ -93,13 +93,13 @@

Source: infrastructure/collections/LeaCollection.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/infrastructure_collections_collections.js.html b/docs/api/app/infrastructure_collections_collections.js.html index 87e551b5..b8468da8 100644 --- a/docs/api/app/infrastructure_collections_collections.js.html +++ b/docs/api/app/infrastructure_collections_collections.js.html @@ -67,13 +67,13 @@

Source: infrastructure/collections/collections.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/infrastructure_createCollection.js.html b/docs/api/app/infrastructure_createCollection.js.html index 67f2335e..1c4103b2 100644 --- a/docs/api/app/infrastructure_createCollection.js.html +++ b/docs/api/app/infrastructure_createCollection.js.html @@ -64,13 +64,13 @@

Source: infrastructure/createCollection.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/infrastructure_createRepository.js.html b/docs/api/app/infrastructure_createRepository.js.html index b3dd820e..d38262d7 100644 --- a/docs/api/app/infrastructure_createRepository.js.html +++ b/docs/api/app/infrastructure_createRepository.js.html @@ -58,13 +58,13 @@

Source: infrastructure/createRepository.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/infrastructure_log_InteractionGraph.js.html b/docs/api/app/infrastructure_log_InteractionGraph.js.html index ba387364..ede3a69f 100644 --- a/docs/api/app/infrastructure_log_InteractionGraph.js.html +++ b/docs/api/app/infrastructure_log_InteractionGraph.js.html @@ -166,13 +166,13 @@

Source: infrastructure/log/InteractionGraph.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/infrastructure_sync_Sync.js.html b/docs/api/app/infrastructure_sync_Sync.js.html index 1581e2fc..9cf89b8c 100644 --- a/docs/api/app/infrastructure_sync_Sync.js.html +++ b/docs/api/app/infrastructure_sync_Sync.js.html @@ -244,13 +244,13 @@

Source: infrastructure/sync/Sync.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/items_choice_ChoiceRenderer.js.html b/docs/api/app/items_choice_ChoiceRenderer.js.html index 7f308de6..0c20066c 100644 --- a/docs/api/app/items_choice_ChoiceRenderer.js.html +++ b/docs/api/app/items_choice_ChoiceRenderer.js.html @@ -208,13 +208,13 @@

Source: items/choice/ChoiceRenderer.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/items_choice_getChoiceEntryScoreColor.js.html b/docs/api/app/items_choice_getChoiceEntryScoreColor.js.html index e9194729..144fd156 100644 --- a/docs/api/app/items_choice_getChoiceEntryScoreColor.js.html +++ b/docs/api/app/items_choice_getChoiceEntryScoreColor.js.html @@ -71,13 +71,13 @@

Source: items/choice/getChoiceEntryScoreColor.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/items_cloze_Cloze.js.html b/docs/api/app/items_cloze_Cloze.js.html index 362692c8..b128a677 100644 --- a/docs/api/app/items_cloze_Cloze.js.html +++ b/docs/api/app/items_cloze_Cloze.js.html @@ -91,13 +91,13 @@

Source: items/cloze/Cloze.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/items_cloze_ClozeRenderer.js.html b/docs/api/app/items_cloze_ClozeRenderer.js.html index a6823c31..816b026d 100644 --- a/docs/api/app/items_cloze_ClozeRenderer.js.html +++ b/docs/api/app/items_cloze_ClozeRenderer.js.html @@ -491,13 +491,13 @@

Source: items/cloze/ClozeRenderer.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/items_cloze_ClozeRendererBlank.js.html b/docs/api/app/items_cloze_ClozeRendererBlank.js.html index 0f45633a..945ae9a9 100644 --- a/docs/api/app/items_cloze_ClozeRendererBlank.js.html +++ b/docs/api/app/items_cloze_ClozeRendererBlank.js.html @@ -251,13 +251,13 @@

Source: items/cloze/ClozeRendererBlank.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/items_cloze_ClozeRendererSelect.js.html b/docs/api/app/items_cloze_ClozeRendererSelect.js.html index 39e782ec..11fc1f03 100644 --- a/docs/api/app/items_cloze_ClozeRendererSelect.js.html +++ b/docs/api/app/items_cloze_ClozeRendererSelect.js.html @@ -311,13 +311,13 @@

Source: items/cloze/ClozeRendererSelect.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/items_cloze_ClozeTokenizer.js.html b/docs/api/app/items_cloze_ClozeTokenizer.js.html index b13d4920..9e3e2fbf 100644 --- a/docs/api/app/items_cloze_ClozeTokenizer.js.html +++ b/docs/api/app/items_cloze_ClozeTokenizer.js.html @@ -239,13 +239,13 @@

Source: items/cloze/ClozeTokenizer.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/items_cloze_createScoringSummaryForInput.js.html b/docs/api/app/items_cloze_createScoringSummaryForInput.js.html index 4fb87a8f..0b0865f4 100644 --- a/docs/api/app/items_cloze_createScoringSummaryForInput.js.html +++ b/docs/api/app/items_cloze_createScoringSummaryForInput.js.html @@ -53,7 +53,7 @@

Source: items/cloze/createScoringSummaryForInput.js

index: itemIndex, score: 0, // computed average color: undefined, - actual: actual, + actual, entries: [] } @@ -86,13 +86,13 @@

Source: items/cloze/createScoringSummaryForInput.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/items_connect_ConnectItemRenderer.js.html b/docs/api/app/items_connect_ConnectItemRenderer.js.html index 0578685e..9f832a37 100644 --- a/docs/api/app/items_connect_ConnectItemRenderer.js.html +++ b/docs/api/app/items_connect_ConnectItemRenderer.js.html @@ -26,19 +26,22 @@

Source: items/connect/ConnectItemRenderer.js

-
import React, { useRef, useState, useEffect } from 'react'
+            
import React, { useRef, useState, useEffect, useCallback } from 'react'
 import {
-  ActivityIndicator,
   Pressable,
   View
 } from 'react-native'
 import { createStyleSheet } from '../../styles/createStyleSheet'
 import { LeaText } from '../../components/LeaText'
 import { Colors } from '../../constants/Colors'
-import { Svg, Path, Circle } from 'react-native-svg'
+import { Svg, Circle, G } from 'react-native-svg'
 import { UndefinedScore } from '../../scoring/UndefinedScore'
 import { clearObject } from '../../utils/object/clearObject'
 import { ImageRenderer } from '../../components/renderer/media/ImageRenderer'
+import { Log } from '../../infrastructure/Log'
+import { DashedLine } from '../../components/animated/DashedLine'
+
+const debug = Log.create('ConnectItemRenderer', 'debug')
 
 /**
  * Renders a connect item, where users have to connect
@@ -66,7 +69,12 @@ 

Source: items/connect/ConnectItemRenderer.js

const [selected, setSelected] = useState({}) const [highlighted, setHighlighted] = useState(initialHighlighted()) const [compared, setCompared] = useState(initialCompared()) - const svgContainer = useRef({}).current + const svgContainer = useRef({ + x: 0, + y: 0, + w: 100, + h: 100 + }).current const leftDots = useRef({}).current const rightDots = useRef({}).current const [active, setActive] = useState(null) @@ -99,6 +107,13 @@

Source: items/connect/ConnectItemRenderer.js

data: props }) }, [props.contentId]) + const updateSvgContainer = ({ x, y, w, h }) => { + debug('SVG Container set layout', { x, y, w, h }) + svgContainer.x = x + svgContainer.y = y + svgContainer.w = w + svgContainer.h = h + } // on showCorrectResponse, do: // compare responses with the correct responses when @@ -177,34 +192,33 @@

Source: items/connect/ConnectItemRenderer.js

// to get the correct coordinates; // event.nativeEvent.layout would only give // us the local coordinates + event.target.measureInWindow((x, y, w, h) => { - svgContainer.x = x - svgContainer.y = y - svgContainer.w = w - svgContainer.h = h + updateSvgContainer({ x, y, w, h }) }) } // measures the position of the invisible dots, // which are used to get the start and end points // to draw the connections - const onDotLayout = (event, id, isLeft) => { - event.target.measureInWindow((x, y) => { - const target = isLeft - ? leftDots - : rightDots - target[id] = { x, y } - - if ( - Object.keys(leftDots).length === props.value.left.length && - Object.keys(rightDots).length === props.value.right.length - ) { - setTimeout(() => setComplete(true), 500) - } + const onDotLayout = useCallback((event, id, isLeft) => { + const stamp = Date.now() + const target = isLeft + ? leftDots + : rightDots + + debug('dot layout', isLeft ? 'L' : 'R', id, event.nativeEvent.layout, event.target.key) + event.target.measureInWindow((x, y, w, h) => { + const centerX = isLeft ? w + 5 : x + const centerY = y + (h / 2) + debug('dot measure', isLeft ? 'L' : 'R', id, { x, y, w, h, centerX, centerY }, stamp) + + target[id] = { x: centerX, y: centerY } }) - } + }, []) - const onPressLeft = ({ id }) => { + const onPressLeft = useCallback(({ id }) => { + if (!complete) { setComplete(true) } if (props.showCorrectResponse) { return updateHighlighted(id, 'left') } @@ -215,7 +229,7 @@

Source: items/connect/ConnectItemRenderer.js

else { setActive(id) } - } + }, [active, props.showCorrectResponse]) const onPressRight = ({ id }) => { if (props.showCorrectResponse) { @@ -276,6 +290,8 @@

Source: items/connect/ConnectItemRenderer.js

data: props }) + if (!complete) { setComplete(true) } + setSelected(current) setActive(null) } @@ -337,10 +353,9 @@

Source: items/connect/ConnectItemRenderer.js

const renderLeftElements = () => { return props.value.left.map(({ text, image }, index) => { - const nodeId = `left-${index}` + const nodeStyle = [styles.node] const isActive = active === index && !props.showCorrectResponse const isSelected = index in selected && !props.showCorrectResponse - const nodeStyle = [styles.node] if (isSelected) { nodeStyle.push(styles.nodeSelected) @@ -357,10 +372,11 @@

Source: items/connect/ConnectItemRenderer.js

return ( <Pressable - accessibilityRole='button' - key={nodeId} + accessibilityRole="button" + key={`left-${index}`} style={styles.nodeContainer} onPress={() => onPressLeft({ id: index })} + onLayout={e => !complete && onDotLayout(e, index, true)} > <View style={nodeStyle}> { @@ -369,10 +385,6 @@

Source: items/connect/ConnectItemRenderer.js

: renderText({ isSelected, text }) } </View> - <View - style={styles.dot} - onLayout={(event) => onDotLayout(event, index, true)} - /> </Pressable> ) }) @@ -380,8 +392,7 @@

Source: items/connect/ConnectItemRenderer.js

const renderLines = () => { const allLines = [] - - const transformToLine = color => key => { + const transformToLine = (color, responseType) => key => { const [left, right] = key.split(',') const x1 = leftDots[left].x const y1 = leftDots[left].y @@ -393,7 +404,7 @@

Source: items/connect/ConnectItemRenderer.js

opacity = getHighlightedStyle({ index: key, type: 'line' }) } - allLines.push({ x1, y1, x2, y2, color, opacity }) + allLines.push({ x1, y1, x2, y2, color, opacity, responseType }) } if (props.showCorrectResponse) { @@ -411,40 +422,31 @@

Source: items/connect/ConnectItemRenderer.js

const y2 = rightDots[right].y const color = props.dimensionColor const opacity = 1.0 - allLines.push({ x1, y1, x2, y2, color, opacity }) }) }) } return allLines.map((position, index) => { - const itemKey = `svg-${index}` + const lineKey = `svg-line-${index}` return ( - <Svg - key={itemKey} style={styles.svg} width={svgContainer.w} height={svgContainer.h} - viewBox={`${svgContainer.x} ${svgContainer.y} ${svgContainer.w} ${svgContainer.h}`} - > - <Path - strokeWidth='4' - stroke={position.color} - strokeOpacity={position.opacity} - d={`M ${position.x1} ${position.y1} L ${position.x2} ${position.y2}`} - /> - <Circle - x={position.x2} - y={position.y2} - r={10} - fill={position.color} - fillOpacity={position.opacity} - /> - </Svg> + <Connection + key={lineKey} + invert={true} + width="4" + color={position.color} + opacity={position.opacity} + startX={position.x1} + startY={position.y1} + endX={position.x2} + endY={position.y2} + /> ) }) } const renderRightElements = () => { return props.value.right.map(({ text, image }, index) => { - const nodeId = `right-${index}` const nodeStyle = [styles.node] const isSelected = ( !props.showCorrectResponse && @@ -462,15 +464,12 @@

Source: items/connect/ConnectItemRenderer.js

return ( <Pressable - accessibilityRole='button' - key={nodeId} + accessibilityRole="button" + key={`right-${index}`} style={styles.nodeContainer} onPress={() => onPressRight({ id: index })} + onLayout={e => !complete && onDotLayout(e, index, false)} > - <View - style={styles.dot} - onLayout={(event) => onDotLayout(event, index, false)} - /> <View style={nodeStyle}> { image @@ -485,14 +484,19 @@

Source: items/connect/ConnectItemRenderer.js

return ( <View style={[styles.overlay, props.style]} onLayout={onContainerLayout}> - {renderLines()} + <Svg + style={styles.svg} + width={svgContainer.w} + height={svgContainer.h} + viewBox={`${svgContainer.x} ${svgContainer.y} ${svgContainer.w} ${svgContainer.h}`} + > + {renderLines()} + </Svg> <View style={styles.container}> <View style={styles.leftContainer}> {renderLeftElements()} </View> - <View style={styles.centerContainer}> - {!complete && <ActivityIndicator color={props.dimensionColor} />} - </View> + <View style={styles.centerContainer}/> <View style={styles.rightContainer}> {renderRightElements()} </View> @@ -501,6 +505,35 @@

Source: items/connect/ConnectItemRenderer.js

) } +const Connection = React.memo(props => { + const path = `M ${props.startX} ${props.startY} L ${props.endX} ${props.endY}` + return ( + <G> + <Circle + x={props.startX} + y={props.startY} + r={5} + fill={props.color} + fillOpacity={props.opacity} + /> + <DashedLine + invert={props.invert} + width={props.width} + color={props.color} + opacity={props.opacity} + path={path} + /> + <Circle + x={props.endX} + y={props.endY} + r={5} + fill={props.color} + fillOpacity={props.opacity} + /> + </G> + ) +}) + const renderText = ({ isSelected, text }) => ( <LeaText fitSize @@ -540,7 +573,6 @@

Source: items/connect/ConnectItemRenderer.js

overlay: { marginTop: 25, borderColor: '#ff0' - }, svgContainer: { borderColor: '#0f0' @@ -569,7 +601,7 @@

Source: items/connect/ConnectItemRenderer.js

}, centerContainer: { flex: 1, - maxWidth: '15%', + maxWidth: '33%', flexDirection: 'column', flexGrow: 1, alignItems: 'center', @@ -581,36 +613,28 @@

Source: items/connect/ConnectItemRenderer.js

justifyContent: 'center' }, node: { - paddingTop: 10, - paddingBottom: 10, + marginTop: 10, + marginBottom: 10, flexGrow: 1, - alignItems: 'stretch', + alignItems: 'center', justifyContent: 'center', - backgroundColor: Colors.transparent, + backgroundColor: Colors.white, borderColor: Colors.secondary, + borderWidth: 1, borderRadius: 5 }, nodeSelected: { backgroundColor: Colors.secondary, borderColor: Colors.secondary }, - highlightActive: { - - }, + highlightActive: {}, highlightPassive: { opacity: 0.1 }, - dot: { - width: 5, - height: 5, - backgroundColor: Colors.transparent, - borderRadius: 15 - }, textSelected: { color: Colors.white }, - text: { - }, + text: {}, textElement: {}, image: { width: '100%' @@ -629,13 +653,13 @@

Source: items/connect/ConnectItemRenderer.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/items_shared_getCompareValuesForSelectableItems.js.html b/docs/api/app/items_shared_getCompareValuesForSelectableItems.js.html index 660691ab..69d5b4e6 100644 --- a/docs/api/app/items_shared_getCompareValuesForSelectableItems.js.html +++ b/docs/api/app/items_shared_getCompareValuesForSelectableItems.js.html @@ -70,13 +70,13 @@

Source: items/shared/getCompareValuesForSelectableItems.j
- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/items_utils_CompareState.js.html b/docs/api/app/items_utils_CompareState.js.html index 7d17f842..3429062f 100644 --- a/docs/api/app/items_utils_CompareState.js.html +++ b/docs/api/app/items_utils_CompareState.js.html @@ -82,13 +82,13 @@

Source: items/utils/CompareState.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/items_utils_KeyboardTypes.js.html b/docs/api/app/items_utils_KeyboardTypes.js.html index 2e9b8886..f65c050a 100644 --- a/docs/api/app/items_utils_KeyboardTypes.js.html +++ b/docs/api/app/items_utils_KeyboardTypes.js.html @@ -92,13 +92,13 @@

Source: items/utils/KeyboardTypes.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/meteor_call.js.html b/docs/api/app/meteor_call.js.html index 45806190..06a61104 100644 --- a/docs/api/app/meteor_call.js.html +++ b/docs/api/app/meteor_call.js.html @@ -28,7 +28,7 @@

Source: meteor/call.js

import Meteor from '@meteorrn/core'
 import { check } from '../schema/check'
-import { ensureConnected } from './ensureConnected'
+
 import { MeteorError } from '../errors/MeteorError'
 import { Log } from '../infrastructure/Log'
 import { createSchema } from '../schema/createSchema'
@@ -67,7 +67,6 @@ 

Source: meteor/call.js

*/ export const callMeteor = (options) => { check(options, callMethodSchema) - ensureConnected() const { name, args = undefined, @@ -115,21 +114,24 @@

Source: meteor/call.js

* @see {callMeteor} */ const call = ({ name, args, prepare, receive }) => { + const isConnected = Meteor.status()?.status === 'connected' const promise = new Promise((resolve, reject) => { // inform that we are connected and about to call the server if (typeof prepare === 'function') { prepare() } - Meteor.call(name, args, (error, result) => { - // inform that we have received - // something from the server - if (typeof receive === 'function') { receive() } + Meteor.getData().waitDdpConnected(() => { + Meteor.call(name, args, (error, result) => { + // inform that we have received + // something from the server + if (typeof receive === 'function') { receive() } - if (error) { - // we convert server responses to MeteorError - return reject(MeteorError.from(error)) - } + if (error) { + // we convert server responses to MeteorError + return reject(MeteorError.from(error)) + } - return resolve(result) + return resolve(result) + }) }) }) @@ -138,7 +140,12 @@

Source: meteor/call.js

// let the promise race against a timeout to ensure // our UI remains responsive in case we didn't get any // response from the server - return createTimedPromise(promise, options) + // + // XXX: if we are disconnected then we skip the timeout + // as we can't really know whether reconnect will be in time + return isConnected + ? createTimedPromise(promise, options) + : promise } const optionalFunction = { type: Function, optional: true } @@ -168,13 +175,13 @@

Source: meteor/call.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/meteor_ensureConnected.js.html b/docs/api/app/meteor_ensureConnected.js.html index def8a161..df12f4cb 100644 --- a/docs/api/app/meteor_ensureConnected.js.html +++ b/docs/api/app/meteor_ensureConnected.js.html @@ -52,13 +52,13 @@

Source: meteor/ensureConnected.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/meteor_updateUserProfile.js.html b/docs/api/app/meteor_updateUserProfile.js.html index cfa3c1aa..ba1ed214 100644 --- a/docs/api/app/meteor_updateUserProfile.js.html +++ b/docs/api/app/meteor_updateUserProfile.js.html @@ -55,13 +55,13 @@

Source: meteor/updateUserProfile.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/meteor_useDocs.js.html b/docs/api/app/meteor_useDocs.js.html index 0797473e..e58b85d3 100644 --- a/docs/api/app/meteor_useDocs.js.html +++ b/docs/api/app/meteor_useDocs.js.html @@ -27,11 +27,11 @@

Source: meteor/useDocs.js

import { useEffect, useState } from 'react'
-import { InteractionGraph } from '../infrastructure/log/InteractionGraph'
 import { ErrorReporter } from '../errors/ErrorReporter'
 import { Log } from '../infrastructure/Log'
-
-const MAX_ATTEMPTS = 3
+import { isDefined } from '../utils/object/isDefined'
+import { asyncTimeout } from '../utils/asyncTimeout'
+import { useTranslation } from 'react-i18next'
 
 // TODO move to hooks folder
 
@@ -50,52 +50,88 @@ 

Source: meteor/useDocs.js

* @param runArgs {array=} optional run args for the internal useEffect * @param debug {boolean=} optional boolean flag for debugging * @param allArgsRequired {boolean=} optional boolean flag to skip loading until all given args are non-null + * @param reload {number?} optional count that can invoke a new load cycle, usually incremented when the + * @param dataRequired {boolean?} optional flag, leading to throw an error, if fn returns null/undefined + * @param maxAttempts {number?} optional count, allows to run fn multiple times before continueing * @return {{data: undefined, error: undefined, loading: boolean}} */ -export const useDocs = ({ fn, runArgs = [], debug = false, allArgsRequired = false }) => { +export const useDocs = ({ + fn, + runArgs = [], + debug = false, + allArgsRequired = false, + dataRequired = false, + reload = 0, + maxAttempts = 1, + message +}) => { + const { t } = useTranslation() const [data, setData] = useState() const [error, setError] = useState() const [loading, setLoading] = useState(true) + const [loadMessage, setLoadMessage] = useState() + + useEffect(() => { + if (message) { + setLoadMessage(t(message)) + } + }, [message]) useEffect(() => { if (allArgsRequired && runArgs.some(arg => arg === null)) { return } - let attempts = 0 - const load = async function () { - try { - const data = await fn(debug) - setData(data) - setLoading(false) - } - catch (e) { - attempts++ + const loadWrapper = async () => { + let error + let attempts = 0 + + setLoading(true) + setError(null) - if (attempts >= MAX_ATTEMPTS) { - throw e + // enable states to take effect + // in consuming components, so users + // are aware we are (re-)loading + await asyncTimeout(300) + + while (attempts < maxAttempts) { + try { + return await load() + } + catch (e) { + error = e } - else { - return load() + finally { + attempts++ } } + error.attempts = attempts + throw error } - load().catch(e => { - e.details = { attempts, env: useDocs.name, fn: String(fn) } - ErrorReporter.send({ error: e }).catch(Log.error) - InteractionGraph.problem({ - type: 'loadFailed', - target: useDocs.name, - error: e, - details: { attempts } + const load = async function () { + const result = await fn(debug) + if (dataRequired && !isDefined(result)) { + throw new Error('errors.noDataReceived') + } + return result + } + + loadWrapper() + .then(result => { + setData(result) + setError(null) + }) + .catch(e => { + const { attempts = 1 } = e + e.details = { attempts, env: useDocs.name, fn: String(fn) } + ErrorReporter.send({ error: e }).catch(Log.error) + setError(e) }) - setError(e) - setLoading(false) - }) - }, runArgs) + .finally(() => setLoading(false)) + }, [...runArgs, reload]) - return { data, error, loading } + return { data, error, loading, loadMessage } }
@@ -107,13 +143,13 @@

Source: meteor/useDocs.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/navigation_MainNavigation.js.html b/docs/api/app/navigation_MainNavigation.js.html index b9da12fe..2c23cf55 100644 --- a/docs/api/app/navigation_MainNavigation.js.html +++ b/docs/api/app/navigation_MainNavigation.js.html @@ -59,6 +59,7 @@

Source: navigation/MainNavigation.js

import { initAppSession } from '../startup/initAppSession' import { getHeaderOptions } from './getHeaderOptions' import { LoggingScreen } from '../screens/logging/LoggingScreen' +import { TTSProfileScreen } from '../screens/profile/tts/TTSProfileScreen' const { AppSessionProvider } = initAppSession() @@ -80,19 +81,20 @@

Source: navigation/MainNavigation.js

export const MainNavigation = (props) => { useKeepAwake() const { t } = useTranslation() - const { state, authContext } = useLogin() + const { state, authContext } = useLogin({ connection: props.connection }) const { Tts } = useTts() const { userToken, isSignout, isDeleted } = state + const renderTitleTts = text => ( <Tts align='center' fontStyle={styles.titleFont} text={text} /> ) - const renderScreens = () => { if (userToken && !isSignout && !isDeleted) { const headerRight = () => (<ProfileButton route='profile' />) const mapScreenTitle = t('mapScreen.title') const profileScreenTitle = t('profileScreen.headerTitle') const achievementScreenTitle = t('profileScreen.achievements.title') + const ttsProfileScreenTitle = t('profileScreen.tts.title') const screens = [ <Stack.Screen name='home' @@ -177,6 +179,21 @@

Source: navigation/MainNavigation.js

headerRight: () => (<></>) }} />, + <Stack.Screen + name='ttsprofile' + key='ttsprofile' + component={TTSProfileScreen} + options={{ + ...headerOptions, + headerStyle, + title: ttsProfileScreenTitle, + headerBackVisible: false, + headerTitleAlign: 'center', + headerLeft: () => (<BackButton icon='arrow-left' />), + headerTitle: () => renderTitleTts(ttsProfileScreenTitle), + headerRight: () => (<></>) + }} + />, <Stack.Screen name='achievements' key='achievements' @@ -379,13 +396,13 @@

Source: navigation/MainNavigation.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/remotes_ContentServer.js.html b/docs/api/app/remotes_ContentServer.js.html index 4f5c192f..e8e15f60 100644 --- a/docs/api/app/remotes_ContentServer.js.html +++ b/docs/api/app/remotes_ContentServer.js.html @@ -57,13 +57,13 @@

Source: remotes/ContentServer.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/schema_createSchema.js.html b/docs/api/app/schema_createSchema.js.html index 6428b024..870f3c30 100644 --- a/docs/api/app/schema_createSchema.js.html +++ b/docs/api/app/schema_createSchema.js.html @@ -63,13 +63,13 @@

Source: schema/createSchema.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/schema_validateSettingsSchema.js.html b/docs/api/app/schema_validateSettingsSchema.js.html index 3b6c062b..ee3115dc 100644 --- a/docs/api/app/schema_validateSettingsSchema.js.html +++ b/docs/api/app/schema_validateSettingsSchema.js.html @@ -27,7 +27,7 @@

Source: schema/validateSettingsSchema.js

import { settingsSchema } from '../settingsSchema'
-import settings from '../settings.json'
+import settings from '../../settings/settings.json'
 
 /**
  * Validate the settings.json file against the
@@ -46,13 +46,13 @@ 

Source: schema/validateSettingsSchema.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/scoring_Scoring.js.html b/docs/api/app/scoring_Scoring.js.html index bb86b1ef..b7b3db3e 100644 --- a/docs/api/app/scoring_Scoring.js.html +++ b/docs/api/app/scoring_Scoring.js.html @@ -116,13 +116,13 @@

Source: scoring/Scoring.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/scoring_ScoringTypes.js.html b/docs/api/app/scoring_ScoringTypes.js.html index d9810cb9..4f022c71 100644 --- a/docs/api/app/scoring_ScoringTypes.js.html +++ b/docs/api/app/scoring_ScoringTypes.js.html @@ -68,13 +68,13 @@

Source: scoring/ScoringTypes.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/scoring_getScoring.js.html b/docs/api/app/scoring_getScoring.js.html index d7a8118f..3ee85377 100644 --- a/docs/api/app/scoring_getScoring.js.html +++ b/docs/api/app/scoring_getScoring.js.html @@ -60,13 +60,13 @@

Source: scoring/getScoring.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/scoring_isUndefinedResponse.js.html b/docs/api/app/scoring_isUndefinedResponse.js.html index 6a84d291..08fcf905 100644 --- a/docs/api/app/scoring_isUndefinedResponse.js.html +++ b/docs/api/app/scoring_isUndefinedResponse.js.html @@ -67,13 +67,13 @@

Source: scoring/isUndefinedResponse.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_BaseScreen.js.html b/docs/api/app/screens_BaseScreen.js.html index cfca018f..9f53099a 100644 --- a/docs/api/app/screens_BaseScreen.js.html +++ b/docs/api/app/screens_BaseScreen.js.html @@ -26,12 +26,15 @@

Source: screens/BaseScreen.js

-
import React from 'react'
+            
import React, { useCallback } from 'react'
 import { Loading } from '../components/Loading'
-import { SafeAreaView } from 'react-native'
+import { RefreshControl, SafeAreaView, ScrollView } from 'react-native'
 import { ErrorMessage } from '../components/ErrorMessage'
 import { LinearProgress } from 'react-native-elements'
 import { Colors } from '../constants/Colors'
+import { createStyleSheet } from '../styles/createStyleSheet'
+import { Layout } from '../constants/Layout'
+import { Log } from '../infrastructure/Log'
 
 /**
  *
@@ -43,9 +46,23 @@ 

Source: screens/BaseScreen.js

* @param props.data {*=} * @param props.progress {number=} * @param props.loadMessage {string} + * @param props.onRefresh {function?} * @return {JSX.Element} */ const RenderScreenBase = (props) => { + const [refreshing, setRefreshing] = React.useState(false) + + const onRefresh = useCallback(async () => { + setRefreshing(true) + try { + await props.onRefresh() + } + catch (e) { + Log.error(e) + } + setRefreshing(false) + }, [props.onRefresh]) + if (props.loading) { return ( <SafeAreaView style={props.style}> @@ -58,7 +75,14 @@

Source: screens/BaseScreen.js

if (props.error) { return ( <SafeAreaView style={props.style}> - <ErrorMessage error={props.error} /> + <ScrollView + refreshControl={ + props.onRefresh && <RefreshControl refreshing={refreshing} onRefresh={onRefresh} /> + } + contentContainerStyle={styles.scrollContainer} + > + <ErrorMessage error={props.error} /> + </ScrollView> </SafeAreaView> ) } @@ -69,7 +93,14 @@

Source: screens/BaseScreen.js

if (loadFailed) { return ( <SafeAreaView style={props.style}> - <ErrorMessage error={new Error('screenBase.notData')} /> + <ScrollView + refreshControl={ + props.onRefresh && <RefreshControl refreshing={refreshing} onRefresh={onRefresh} /> + } + contentContainerStyle={styles.scrollContainer} + > + <ErrorMessage error={new Error('screenBase.notData')} /> + </ScrollView> </SafeAreaView> ) } @@ -77,6 +108,14 @@

Source: screens/BaseScreen.js

return (<SafeAreaView style={props.style}>{props.children}</SafeAreaView>) } +const styles = createStyleSheet({ + scrollContainer: { + ...Layout.container(), + flexGrow: 1, + flex: 0 + } +}) + const linearProgress = progress => { if (typeof progress !== 'number') { return null @@ -98,13 +137,13 @@

Source: screens/BaseScreen.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_auth_RegistrationScreen.js.html b/docs/api/app/screens_auth_RegistrationScreen.js.html index 8e9261d5..403b088e 100644 --- a/docs/api/app/screens_auth_RegistrationScreen.js.html +++ b/docs/api/app/screens_auth_RegistrationScreen.js.html @@ -37,7 +37,7 @@

Source: screens/auth/RegistrationScreen.js

import { Layout } from '../../constants/Layout' import { Loading } from '../../components/Loading' import { InteractionGraph } from '../../infrastructure/log/InteractionGraph' - +import { useRoute } from '@react-navigation/native' /** * Screen for registering a new user. * This screen should automatically run without further actions required. @@ -45,10 +45,11 @@

Source: screens/auth/RegistrationScreen.js

* @component * @returns {JSX.Element} */ -export const RegistrationScreen = () => { +export const RegistrationScreen = (props) => { const { t } = useTranslation() const { Tts } = useTts() const { user } = useUser() + const route = useRoute() const { signUp } = useContext(AuthContext) const [error, setError] = useState(null) @@ -69,7 +70,14 @@

Source: screens/auth/RegistrationScreen.js

type: 'registered' }) - setTimeout(() => signUp({ voice, speed, onError, onSuccess }), 1000) + const { termsAndConditionsIsChecked } = (route ?? {}) + setTimeout(() => signUp({ + termsAndConditionsIsChecked, + voice, + speed, + onError, + onSuccess + }), 1000) } }, [user]) @@ -106,13 +114,13 @@

Source: screens/auth/RegistrationScreen.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_auth_TermsAndConditionsScreen.js.html b/docs/api/app/screens_auth_TermsAndConditionsScreen.js.html index 7d6328e6..3e9603e6 100644 --- a/docs/api/app/screens_auth_TermsAndConditionsScreen.js.html +++ b/docs/api/app/screens_auth_TermsAndConditionsScreen.js.html @@ -44,6 +44,7 @@

Source: screens/auth/TermsAndConditionsScreen.js

const initialState = { termsAndConditionsIsChecked: false, + researchIsChecked: false, highlightCheckbox: false, modalOpen: false } @@ -89,9 +90,14 @@

Source: screens/auth/TermsAndConditionsScreen.js

InteractionGraph.action({ type: 'select', target: checkboxHandler.name, details: { type, value: currentValue } }) + const options = { type, [type]: !currentValue } dispatch(options) + if (type === 'research') { + dispatch({ type: 'research', research: true }) + } + if (type === 'terms' && !currentValue) { dispatch({ type: 'highlight', highlight: false }) } @@ -290,13 +296,13 @@

Source: screens/auth/TermsAndConditionsScreen.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_auth_WelcomeScreen.js.html b/docs/api/app/screens_auth_WelcomeScreen.js.html index cae01bd2..35a4f625 100644 --- a/docs/api/app/screens_auth_WelcomeScreen.js.html +++ b/docs/api/app/screens_auth_WelcomeScreen.js.html @@ -136,13 +136,13 @@

Source: screens/auth/WelcomeScreen.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_complete_CompleteScreen.js.html b/docs/api/app/screens_complete_CompleteScreen.js.html index 5ad19e49..6bf0d434 100644 --- a/docs/api/app/screens_complete_CompleteScreen.js.html +++ b/docs/api/app/screens_complete_CompleteScreen.js.html @@ -34,7 +34,7 @@

Source: screens/complete/CompleteScreen.js

import { useTranslation } from 'react-i18next' import { useDocs } from '../../meteor/useDocs' import { loadCompleteData } from './loadCompleteData' -import { useTts } from '../../components/Tts' +import { TTSengine, useTts } from '../../components/Tts' import { getDimensionColor } from '../unit/getDimensionColor' import { ActionButton } from '../../components/ActionButton' import { Celebrate } from './Celebrate' @@ -51,7 +51,7 @@

Source: screens/complete/CompleteScreen.js

const COMPLETE = 'complete' -Sound.load(COMPLETE, () => require('../../assets/audio/trophy_animation.mp3')) +Sound.load(COMPLETE, () => require('../../../assets/audio/trophy_animation.mp3')) /** * This screen is shown, when no Units are in the queue anymore and the @@ -80,6 +80,7 @@

Source: screens/complete/CompleteScreen.js

// --------------------------------------------------------------------------- useEffect(() => { Vibration.vibrate(1000) + TTSengine.stop() Sound.play(COMPLETE).catch(Log.error) return () => Sound.unload() @@ -258,13 +259,13 @@

Source: screens/complete/CompleteScreen.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_home_HomeScreen.js.html b/docs/api/app/screens_home_HomeScreen.js.html index 3618a99d..282fc4f8 100644 --- a/docs/api/app/screens_home_HomeScreen.js.html +++ b/docs/api/app/screens_home_HomeScreen.js.html @@ -29,6 +29,7 @@

Source: screens/home/HomeScreen.js

import React, { useCallback, useContext } from 'react'
 import { useTts } from '../../components/Tts'
 import { useTranslation } from 'react-i18next'
+import { useRefresh } from '../../hooks/useRefresh'
 import { useDocs } from '../../meteor/useDocs'
 import { loadHomeData } from './loadHomeData'
 import { createStyleSheet } from '../../styles/createStyleSheet'
@@ -59,10 +60,16 @@ 

Source: screens/home/HomeScreen.js

const { Tts } = useTts() const [/* session */, sessionActions] = useContext(AppSessionContext) const { syncRequired, complete, progress } = useSync() - const { data, error, loading } = useDocs({ + const [reload, refresh] = useRefresh() + const { data, error, loading, loadMessage } = useDocs({ fn: () => loadHomeData({ syncRequired, complete }), - runArgs: [syncRequired, complete] + runArgs: [syncRequired, complete], + maxAttempts: 1, + dataRequired: true, + message: 'homeScreen.loading', + reload }) + const selectField = useCallback(async value => { const { _id, title } = value MapIcons.setField(_id) @@ -101,7 +108,14 @@

Source: screens/home/HomeScreen.js

} return ( - <ScreenBase data={data} loading={loading} error={error} style={styles.container}> + <ScreenBase + data={data} + loading={loading} + error={error} + style={styles.container} + loadMessage={loadMessage} + onRefresh={refresh} + > <ScrollView contentContainerStyle={styles.scrollContainer}> <View style={styles.textContainer}> <Tts @@ -159,13 +173,13 @@

Source: screens/home/HomeScreen.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_home_loadHomeData.js.html b/docs/api/app/screens_home_loadHomeData.js.html new file mode 100644 index 00000000..f3cbfd72 --- /dev/null +++ b/docs/api/app/screens_home_loadHomeData.js.html @@ -0,0 +1,75 @@ + + + + + JSDoc: Source: screens/home/loadHomeData.js + + + + + + + + + + +
+ +

Source: screens/home/loadHomeData.js

+ + + + + + +
+
+
import { Field } from '../../contexts/Field'
+import { Order } from '../../contexts/Order'
+import { byOrderedIds } from '../../utils/array/byOrderedIds'
+import { Log } from '../../infrastructure/Log'
+
+/**
+ * Loads the available fields for the home screen.
+ * @return {Promise<object[]>}
+ */
+export const loadHomeData = async () => {
+  const fields = Field.collection().find().fetch()
+  const order = Order.collection().findOne()
+
+  if (Array.isArray(order?.fields) && order.fields.length > 0) {
+    debug('sort fields by order:')
+    Log.print({ fields })
+    Log.print({ 'order.fields': order.fields })
+    fields.sort(byOrderedIds(order.fields))
+  }
+
+  return fields
+}
+
+const debug = Log.create(loadHomeData.name, 'debug')
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time) +
+ + + + + diff --git a/docs/api/app/screens_map_DimensionScreen.js.html b/docs/api/app/screens_map_DimensionScreen.js.html index c715c801..4af11a00 100644 --- a/docs/api/app/screens_map_DimensionScreen.js.html +++ b/docs/api/app/screens_map_DimensionScreen.js.html @@ -209,13 +209,13 @@

Source: screens/map/DimensionScreen.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_map_MapScreen.js.html b/docs/api/app/screens_map_MapScreen.js.html index 6b4054c0..09960508 100644 --- a/docs/api/app/screens_map_MapScreen.js.html +++ b/docs/api/app/screens_map_MapScreen.js.html @@ -31,6 +31,7 @@

Source: screens/map/MapScreen.js

import { createStyleSheet } from '../../styles/createStyleSheet' import { useDocs } from '../../meteor/useDocs' import { loadMapData } from './loadMapData' +import { useRefresh } from '../../hooks/useRefresh' import { Log } from '../../infrastructure/Log' import { useTranslation } from 'react-i18next' import { AppSessionContext } from '../../state/AppSessionContext' @@ -45,9 +46,10 @@

Source: screens/map/MapScreen.js

import { Connector } from './components/Connector' import nextFrame from 'next-frame' import { MapIcons } from '../../contexts/MapIcons' +import { Layout } from '../../constants/Layout' const log = Log.create('MapScreen') -const ITEM_HEIGHT = 100 +const STAGE_SIZE = 100 const counter = 0.75 /** @@ -68,6 +70,9 @@

Source: screens/map/MapScreen.js

const { Tts } = useTts() const [stageConnectorWidth, setStageConnectorWidth] = useState(null) const [activeStage, setActiveStage] = useState(-1) + const [initialIndex, setInitialIndex] = useState(0) + const [listData, setListData] = useState([]) + const [reload, refresh] = useRefresh() const [connectorWidth, setConnectorWidth] = useState(null) const [session, sessionActions] = useContext(AppSessionContext) const mapDocs = useDocs({ @@ -79,7 +84,10 @@

Source: screens/map/MapScreen.js

onUserDataLoaded: () => { sessionActions.update({ loadUserData: null }) } - }) + }), + dataRequired: true, + reload, + message: 'mapScreen.loadData' }) useEffect(() => { @@ -96,11 +104,26 @@

Source: screens/map/MapScreen.js

}) }, [props.navigation, sessionActions]) + useEffect(() => { + const progressIndex = mapDocs?.data?.progressIndex + const entries = mapDocs?.data?.entries + + if (progressIndex && progressIndex !== initialIndex) { + setInitialIndex(progressIndex) + } + + if (entries && listData.length === 0) { + setListData(entries) + } + }, [mapDocs]) + const onListLayoutDetected = useCallback((event) => { const { width } = event.nativeEvent.layout - setStageConnectorWidth(width - ITEM_HEIGHT - (ITEM_HEIGHT / 2)) - setConnectorWidth((width / 2) - ITEM_HEIGHT) - }, [setStageConnectorWidth, setConnectorWidth]) + if (!stageConnectorWidth) { + setStageConnectorWidth(width - STAGE_SIZE - (STAGE_SIZE / 2)) + setConnectorWidth((width / 2) - STAGE_SIZE) + } + }, []) const selectStage = useCallback(async (stage, index) => { setActiveStage(index) @@ -115,10 +138,10 @@

Source: screens/map/MapScreen.js

props.navigation.navigate('dimension') }, [mapDocs]) - const renderListItem = useCallback(({ index, item: entry }) => { + const renderListItem2 = useCallback(({ index, item: entry }) => { if (entry.type === 'stage') { const isActive = activeStage === index - return renderStage({ + const stageData = { index, stage: entry, connectorWidth: stageConnectorWidth, @@ -126,19 +149,20 @@

Source: screens/map/MapScreen.js

isActive, dimensionOrder: mapData?.dimensionOrder, dimensions: mapData?.dimensions - }) + } + return (<MapStage {...stageData} />) } if (entry.type === 'milestone') { - return renderMilestone({ milestone: entry, connectorWidth }) + return (<MapMilestone milestone={entry} connectorWidth={connectorWidth} />) } if (entry.type === 'finish') { return ( <View style={styles.stage}> - {renderConnector(entry.viewPosition.left, connectorWidth)} + <MapConnector connectorId={entry.viewPosition.left} listWidth={connectorWidth} /> <MapFinish /> - {renderConnector(entry.viewPosition.right, connectorWidth)} + <MapConnector connectorId={entry.viewPosition.right} listWidth={connectorWidth} /> </View> ) } @@ -146,9 +170,9 @@

Source: screens/map/MapScreen.js

if (entry.type === 'start') { return ( <View style={styles.stage}> - {renderConnector(entry.viewPosition.left, connectorWidth)} + <MapConnector connectorId={entry.viewPosition.left} listWidth={connectorWidth} /> {MapIcons.render(0)} - {renderConnector(entry.viewPosition.right, connectorWidth)} + <MapConnector connectorId={entry.viewPosition.right} listWidth={connectorWidth} /> </View> ) } @@ -186,58 +210,52 @@

Source: screens/map/MapScreen.js

* } */ const mapData = mapDocs.data - const renderList = () => { - if (!mapData?.entries?.length) { + if (!listData || !stageConnectorWidth) { return null } - - // return mapData.entries.map((item, index) => renderListItem({ index, item })) - return ( - <View style={styles.scrollView}> - <FlatList - data={mapData.entries} - renderItem={renderListItem} - onLayout={onListLayoutDetected} - inverted - decelerationRate='fast' - disableIntervalMomentum - initialScrollIndex={mapData.progressIndex ?? 0} - removeClippedSubviews - persistentScrollbar - keyExtractor={flatListKeyExtractor} - initialNumToRender={50} - maxToRenderPerBatch={50} - updateCellsBatchingPeriod={3000} - getItemLayout={flatListGetItemLayout} - /> - </View> + <FlatList + data={listData} + renderItem={renderListItem2} + inverted + decelerationRate='fast' + disableIntervalMomentum + initialScrollIndex={initialIndex} + removeClippedSubviews + persistentScrollbar + keyExtractor={flatListKeyExtractor} + initialNumToRender={10} + maxToRenderPerBatch={3} + updateCellsBatchingPeriod={50} + getItemLayout={flatListGetItemLayout} + /> ) } - return ( <ScreenBase {...mapDocs} - loadMessage={t('mapScreen.loadData')} progress={counter} - style={styles.container} + onRefresh={refresh} + style={mapDocs.error ? styles.failedContainer : styles.container} > - {renderList()} + <View style={styles.scrollView} onLayout={onListLayoutDetected}> + {renderList()} + </View> </ScreenBase> ) } const flatListGetItemLayout = (data, index) => { - const entry = data[index] - const length = entry && ['stage', 'milestone'].includes(entry.type) - ? ITEM_HEIGHT + 10 - : 59 - return { length, offset: length * index, index } + // const entry = data[index] + // const length = entry && ['stage', 'milestone'].includes(entry.type) + // ? ITEM_HEIGHT + 10 + // : 59 + return { length: STAGE_SIZE, offset: STAGE_SIZE * index, index } } const flatListKeyExtractor = (item) => item.entryKey -const renderStage = ({ index, stage, selectStage, connectorWidth, dimensions, dimensionOrder, isActive }) => { +const MapStage = React.memo(({ index, stage, selectStage, connectorWidth, dimensions, dimensionOrder, isActive }) => { const progress = 100 * (stage.userProgress || 0) / stage.progress const justifyContent = positionMap[stage.viewPosition.current] const stageStyle = mergeStyles(styles.stage, { justifyContent }) @@ -245,10 +263,10 @@

Source: screens/map/MapScreen.js

return ( <View style={stageStyle}> - {renderConnector(viewPosition.left, connectorWidth, viewPosition.icon)} + <MapConnector connectorId={viewPosition.left} listWidth={connectorWidth} withIcon={viewPosition.icon} /> <Stage - width={ITEM_HEIGHT} - height={ITEM_HEIGHT} + width={STAGE_SIZE} + height={STAGE_SIZE} onPress={() => selectStage(stage, index)} unitSets={stage.unitSets} dimensions={dimensions} @@ -257,23 +275,23 @@

Source: screens/map/MapScreen.js

progress={progress} isActive={isActive} /> - {renderConnector(viewPosition.right, connectorWidth, viewPosition.icon)} + <MapConnector connectorId={viewPosition.right} listWidth={connectorWidth} withIcon={viewPosition.icon} /> </View> ) -} +}) -const renderMilestone = ({ milestone, connectorWidth }) => { +const MapMilestone = React.memo(({ milestone, connectorWidth }) => { const progress = 100 * milestone.userProgress / milestone.maxProgress return ( <View style={styles.stage}> - {renderConnector(milestone.viewPosition.left, connectorWidth)} + <MapConnector connectorId={milestone.viewPosition.left} listWidth={connectorWidth} /> <Milestone progress={progress} level={milestone.level + 1} /> - {renderConnector(milestone.viewPosition.right, connectorWidth)} + <MapConnector connectorId={milestone.viewPosition.right} listWidth={connectorWidth} /> </View> ) -} +}) -const renderConnector = (connectorId, listWidth, withIcon = -1) => { +const MapConnector = React.memo(({ connectorId, listWidth, withIcon = -1 }) => { if (connectorId === 'fill') { return ( <LeaText style={{ width: listWidth ?? '100%' }} /> @@ -286,7 +304,8 @@

Source: screens/map/MapScreen.js

} return null -} +}) + const positionMap = { center: 'center', left: 'flex-start', @@ -302,6 +321,9 @@

Source: screens/map/MapScreen.js

marginLeft: 15, marginRight: 15 }, + failedContainer: { + ...Layout.container() + }, scrollView: { width: '100%' }, @@ -310,7 +332,8 @@

Source: screens/map/MapScreen.js

flexDirection: 'row', alignItems: 'center', justifyContent: 'center', - height: ITEM_HEIGHT + height: STAGE_SIZE, + borderColor: 'blue' }, connector: { flexGrow: 1 @@ -332,13 +355,13 @@

Source: screens/map/MapScreen.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_map_components_Connector.js.html b/docs/api/app/screens_map_components_Connector.js.html index 69a2e8a0..9e4a52b0 100644 --- a/docs/api/app/screens_map_components_Connector.js.html +++ b/docs/api/app/screens_map_components_Connector.js.html @@ -26,10 +26,11 @@

Source: screens/map/components/Connector.js

-
import React from 'react'
+            
import React, { useEffect, useState } from 'react'
 import Svg, { G, Path } from 'react-native-svg'
 import { Colors } from '../../../constants/Colors'
 import { MapIcons } from '../../../contexts/MapIcons'
+import { memoize } from '../../../utils/memoize'
 
 /**
  * A connector renders an SVG-based, L-shaped line from one
@@ -47,9 +48,64 @@ 

Source: screens/map/components/Connector.js

* @component */ const ConnectorComponent = props => { - // TODO put in effect + state - const { from, width = 100, height = 100 } = props - const [/* to */, direction = 'up'] = props.to.split('-') + const { from, to, width = 100, height = 100 } = props + const path = usePath(from, to, width, height) + + return ( + <Svg xmlns='http://www.w3.org/2000/svg' width={width} height={height} viewBox={`0 0 ${width} ${height}`}> + <G id='Ebene_1-2' data-name='Ebene 1'> + <Path + className='cls-1' + strokeWidth='2' + strokeMiterlimit='10' + stroke={Colors.gray} + fill={Colors.transparent} + d={path} + /> + <ConnectorIcon width={width} from={from} height={height} icon={props.icon} /> + </G> + </Svg> + ) +} + +const ConnectorIcon = React.memo(props => { + if (typeof props.icon !== 'number') { + return null + } + const { width, from, height, icon } = props + const fromLeft = from === 'left' + const part = width / 7 + const offset = fromLeft + ? part + : part * -1 + const xPos = width / 2 + offset + return ( + <G x={xPos} y={height / 4} width={50} height={50}> + {MapIcons.render(icon)} + </G> + ) +}) + +/** + * + * @param from + * @param to + * @param width + * @param height + * @return {string} + */ +const usePath = (from, to, width, height) => { + const [path, setPath] = useState(null) + useEffect(() => { + const p = pathMemo(from, to, width, height) + setPath(p) + }, [from, to, width, height]) + return path +} + +const [pathMemo] = memoize((...args) => { + const [from, to, width, height] = args + const [/* to */, direction = 'up'] = to.split('-') const halfHeight = Math.round(height / 2) const fromLeft = from === 'left' const startX = fromLeft @@ -71,39 +127,8 @@

Source: screens/map/components/Connector.js

`L ${endX} ${endY}` ] - const renderIcon = () => { - if (typeof props.icon !== 'number') { - return null - } - - const part = width / 7 - const offset = fromLeft - ? part - : part * -1 - const xPos = width / 2 + offset - return ( - <G x={xPos} y={height / 4} width={50} height={50}> - {MapIcons.render(props.icon)} - </G> - ) - } - - return ( - <Svg xmlns='http://www.w3.org/2000/svg' width={width} height={height} viewBox={`0 0 ${width} ${height}`}> - <G id='Ebene_1-2' data-name='Ebene 1'> - <Path - className='cls-1' - strokeWidth='2' - strokeMiterlimit='10' - stroke={Colors.gray} - fill={Colors.transparent} - d={execution.join(' ')} - /> - {renderIcon()} - </G> - </Svg> - ) -} + return execution.join(' ') +}) export const Connector = React.memo(ConnectorComponent)
@@ -116,13 +141,13 @@

Source: screens/map/components/Connector.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_map_components_Milestone.js.html b/docs/api/app/screens_map_components_Milestone.js.html index 248d14dc..3642b56e 100644 --- a/docs/api/app/screens_map_components_Milestone.js.html +++ b/docs/api/app/screens_map_components_Milestone.js.html @@ -82,20 +82,23 @@

Source: screens/map/components/Milestone.js

const xOffsetLeft = ((value - 1) * 9) / 2 for (let i = 0; i < value; i++) { - stars[i] = renderStar(i, xOffsetLeft) + stars[i] = (<MilestoneStar key={i} index={i} xOffsetLeft={xOffsetLeft} />) } return stars } -const renderStar = (index, xOffsetLeft) => ( - <Path - key={`milestone-star-${index}`} - translateX={(index * 9) - xOffsetLeft} - id='Pfad_123-2' data-name='Pfad 123-2' fill='white' - d='M25.37,32.76l-1,2-2.25.32a.5.5,0,0,0-.42.56.48.48,0,0,0,.15.28l1.62,1.59-.38,2.24a.49.49,0,0,0,.4.57.54.54,0,0,0,.31,0l2-1.06,2,1.06a.5.5,0,0,0,.66-.21.49.49,0,0,0,.05-.31l-.39-2.24L29.78,36a.48.48,0,0,0,0-.69.51.51,0,0,0-.28-.15l-2.25-.32-1-2a.48.48,0,0,0-.66-.22.43.43,0,0,0-.22.22Z' - /> -) +const MilestoneStar = React.memo((props) => { + const { index, xOffsetLeft } = props + return ( + <Path + key={`milestone-star-${index}`} + translateX={(index * 9) - xOffsetLeft} + id='Pfad_123-2' data-name='Pfad 123-2' fill='white' + d='M25.37,32.76l-1,2-2.25.32a.5.5,0,0,0-.42.56.48.48,0,0,0,.15.28l1.62,1.59-.38,2.24a.49.49,0,0,0,.4.57.54.54,0,0,0,.31,0l2-1.06,2,1.06a.5.5,0,0,0,.66-.21.49.49,0,0,0,.05-.31l-.39-2.24L29.78,36a.48.48,0,0,0,0-.69.51.51,0,0,0-.28-.15l-2.25-.32-1-2a.48.48,0,0,0-.66-.22.43.43,0,0,0-.22.22Z' + /> + ) +}) export const Milestone = React.memo(MilestoneComponent)
@@ -108,13 +111,13 @@

Source: screens/map/components/Milestone.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_map_components_Stage.js.html b/docs/api/app/screens_map_components_Stage.js.html index 90a7e7d7..95bf5670 100644 --- a/docs/api/app/screens_map_components_Stage.js.html +++ b/docs/api/app/screens_map_components_Stage.js.html @@ -218,13 +218,13 @@

Source: screens/map/components/Stage.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_map_loadMapData.js.html b/docs/api/app/screens_map_loadMapData.js.html index d759bb15..4853780f 100644 --- a/docs/api/app/screens_map_loadMapData.js.html +++ b/docs/api/app/screens_map_loadMapData.js.html @@ -68,12 +68,18 @@

Source: screens/map/loadMapData.js

// 1. for the current field get map data from cache // or load from server, field is required at this step - const mapData = mapCache.has(fieldId) - ? mapCache.get(fieldId) - : await callMeteor({ + let mapData + + if (mapCache.has(fieldId)) { + mapData = mapCache.get(fieldId) + } + + if (!mapData) { + mapData = await callMeteor({ name: Config.methods.getMapData, args: { fieldId } }) + } // 2. if data is incomplete return null // this requires dimensions, levels and entries @@ -88,7 +94,7 @@

Source: screens/map/loadMapData.js

debug('data incomplete, skip with null') debug({ hasData, hasDimensions, hasEntries, hasLevels }) debug({ mapData }) - return null + return { empty: true } } await nextFrame() @@ -359,13 +365,13 @@

Source: screens/map/loadMapData.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_profile_ProfileScreen.js.html b/docs/api/app/screens_profile_ProfileScreen.js.html index b32bbc29..fea2a386 100644 --- a/docs/api/app/screens_profile_ProfileScreen.js.html +++ b/docs/api/app/screens_profile_ProfileScreen.js.html @@ -31,7 +31,6 @@

Source: screens/profile/ProfileScreen.js

import { AccountInfo } from './account/AccountInfo' import { createStyleSheet } from '../../styles/createStyleSheet' import { Layout } from '../../constants/Layout' -import { TTSSettings } from './TTSSettings' import { useTimeout } from '../../hooks/useTimeout' import { Loading } from '../../components/Loading' import { Colors } from '../../constants/Colors' @@ -58,6 +57,7 @@

Source: screens/profile/ProfileScreen.js

return ( <SafeAreaView style={styles.container}> <ActionButton + containerStyle={{ marginTop: 25 }} buttonStyle={styles.achievementsButton} titleStyle={styles.achievementButtonTitle} iconColor={Colors.secondary} @@ -65,16 +65,15 @@

Source: screens/profile/ProfileScreen.js

onPress={() => props.navigation.navigate('achievements')} title={t('profileScreen.achievements.title')} /> - <View style={styles.headline}> - <Tts - text={t('tts.settings')} - color={Colors.secondary} - align='center' - fontStyle={styles.headlineText} - id='profileScreen.tts.settings' - /> - </View> - <TTSSettings containerStyle={styles.tts} /> + <ActionButton + buttonStyle={styles.achievementsButton} + containerStyle={{ marginTop: 25 }} + titleStyle={styles.achievementButtonTitle} + iconColor={Colors.secondary} + color={Colors.white} + onPress={() => props.navigation.navigate('ttsprofile')} + title={t('tts.settings')} + /> <View style={styles.headline}> <Tts text={t('accountInfo.title')} @@ -134,13 +133,13 @@

Source: screens/profile/ProfileScreen.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_profile_account_AccountInfo.js.html b/docs/api/app/screens_profile_account_AccountInfo.js.html index 84b2391f..521f061d 100644 --- a/docs/api/app/screens_profile_account_AccountInfo.js.html +++ b/docs/api/app/screens_profile_account_AccountInfo.js.html @@ -42,7 +42,7 @@

Source: screens/profile/account/AccountInfo.js

import { useDocs } from '../../../meteor/useDocs' import { loadAccountData } from './loadAccountData' import { Markdown } from '../../../components/MarkdownWithTTS' -import { Icon } from 'react-native-elements' +import Icon from '@expo/vector-icons/FontAwesome6' import { AppTerminate } from '../../../infrastructure/app/AppTerminate' import { clearContextStorage } from '../../../contexts/createContextStorage' import { Log } from '../../../infrastructure/Log' @@ -78,7 +78,7 @@

Source: screens/profile/account/AccountInfo.js

}, body: () => ( <> - <Icon name='check' color={Colors.success} type='font-awesome-5' /> + <Icon name='check' color={Colors.success} /> <Tts block text={t('accountInfo.close.next')} /> </> ), @@ -129,7 +129,7 @@

Source: screens/profile/account/AccountInfo.js

instructions: () => t('accountInfo.restore.instructions'), body: () => (<RequestRestoreCodes onError={onError} />), deny: { - icon: 'times', + icon: 'xmark', label: () => t('actions.close'), handler: () => { InteractionGraph.action({ @@ -148,7 +148,7 @@

Source: screens/profile/account/AccountInfo.js

*/ actions.signOut = { key: 'signOut', - icon: 'sign-out-alt', + icon: 'right-from-bracket', label: () => t('accountInfo.signOut.title'), onPress: () => setModalContent(actions.signOut.modal), modal: { @@ -172,7 +172,7 @@

Source: screens/profile/account/AccountInfo.js

} }, deny: { - icon: 'times', + icon: 'xmark', label: () => t('actions.cancel'), handler: () => setModalContent(null) } @@ -186,7 +186,7 @@

Source: screens/profile/account/AccountInfo.js

modal: { body: () => ( <View style={styles.danger}> - <Tts block text={t('accountInfo.deleteAccount.instructions')} color={Colors.danger} /> + <Tts block text={t('accountInfo.deleteAccount.instructions')} color={Colors.secondary} /> </View> ), approve: { @@ -195,26 +195,44 @@

Source: screens/profile/account/AccountInfo.js

color: Colors.danger, titleStyle: styles.dangerText, handler: () => { - const onSuccess = () => { + const onSuccess = async () => { lastAction.current = 'deleted' Sync.reset() - clearContextStorage(onError) - .catch(Log.error) - .then(() => setModalContent(closeModal)) + try { + await clearContextStorage() + } + catch (error) { + onError(error) + } + setModalContent(actions.deleteAccount.deleted) } deleteAccount({ onError, onSuccess }) } }, deny: { - icon: 'times', + icon: 'xmark', label: () => t('actions.cancel'), handler: () => setModalContent(null) } + }, + deleted: { + body: () => ( + <View> + <Tts block text={t('accountInfo.deleteAccount.successful')} color={Colors.secondary} /> + </View> + ), + deny: { + icon: 'home', + label: () => t('actions.toHome'), + handler: () => { + setModalContent(null) + } + } } } actions.privacy = { - icon: 'shield-alt', + icon: 'file-shield', key: 'privacy', label: () => t('legal.privacy'), onPress: () => setModalContent(actions.privacy.modal), @@ -226,7 +244,7 @@

Source: screens/profile/account/AccountInfo.js

) }, deny: { - icon: 'times', + icon: 'xmark', label: () => t('actions.close'), handler: () => setModalContent(null) } @@ -246,7 +264,7 @@

Source: screens/profile/account/AccountInfo.js

) }, deny: { - icon: 'times', + icon: 'xmark', label: () => t('actions.close'), handler: () => setModalContent(null) } @@ -266,7 +284,7 @@

Source: screens/profile/account/AccountInfo.js

) }, deny: { - icon: 'times', + icon: 'xmark', label: () => t('actions.close'), handler: () => setModalContent(null) } @@ -405,13 +423,13 @@

Source: screens/profile/account/AccountInfo.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_profile_achievements_AchievementsScreen.js.html b/docs/api/app/screens_profile_achievements_AchievementsScreen.js.html index 7aeff1b7..3576d598 100644 --- a/docs/api/app/screens_profile_achievements_AchievementsScreen.js.html +++ b/docs/api/app/screens_profile_achievements_AchievementsScreen.js.html @@ -159,13 +159,13 @@

Source: screens/profile/achievements/AchievementsScreen.j
- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_profile_achievements_loadAchievementsData.js.html b/docs/api/app/screens_profile_achievements_loadAchievementsData.js.html index 7baeb3f8..e02df823 100644 --- a/docs/api/app/screens_profile_achievements_loadAchievementsData.js.html +++ b/docs/api/app/screens_profile_achievements_loadAchievementsData.js.html @@ -150,13 +150,13 @@

Source: screens/profile/achievements/loadAchievementsData
- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_sync_SyncScreen.js.html b/docs/api/app/screens_sync_SyncScreen.js.html index 006697c3..140e8645 100644 --- a/docs/api/app/screens_sync_SyncScreen.js.html +++ b/docs/api/app/screens_sync_SyncScreen.js.html @@ -79,13 +79,13 @@

Source: screens/sync/SyncScreen.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_unit_UnitScreen.js.html b/docs/api/app/screens_unit_UnitScreen.js.html index 55e4601a..9b88c865 100644 --- a/docs/api/app/screens_unit_UnitScreen.js.html +++ b/docs/api/app/screens_unit_UnitScreen.js.html @@ -141,12 +141,12 @@

Source: screens/unit/UnitScreen.js

pressable question={t('unitScreen.abort.question')} approveText={t('unitScreen.abort.abort')} - approveIcon='times' + approveIcon='xmark' denyText={t('unitScreen.abort.continue')} - denyIcon='edit' + denyIcon='marker' onApprove={() => cancelUnit()} onDeny={() => {}} - icon='times' + icon='xmark' tts={false} style={styles.confirm} /> @@ -371,7 +371,7 @@

Source: screens/unit/UnitScreen.js

align='center' tts={t('unitScreen.actions.check')} color={dimensionColor} - icon='edit' + icon='marker' onPress={checkScore} /> ) @@ -449,13 +449,13 @@

Source: screens/unit/UnitScreen.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_unit_completeUnit.js.html b/docs/api/app/screens_unit_completeUnit.js.html index ea54b852..aceebd4b 100644 --- a/docs/api/app/screens_unit_completeUnit.js.html +++ b/docs/api/app/screens_unit_completeUnit.js.html @@ -60,13 +60,13 @@

Source: screens/unit/completeUnit.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_unit_createResponseDoc.js.html b/docs/api/app/screens_unit_createResponseDoc.js.html index d51421aa..9890d331 100644 --- a/docs/api/app/screens_unit_createResponseDoc.js.html +++ b/docs/api/app/screens_unit_createResponseDoc.js.html @@ -66,13 +66,13 @@

Source: screens/unit/createResponseDoc.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_unit_getDimensionColor.js.html b/docs/api/app/screens_unit_getDimensionColor.js.html index 6f4312b2..8653090a 100644 --- a/docs/api/app/screens_unit_getDimensionColor.js.html +++ b/docs/api/app/screens_unit_getDimensionColor.js.html @@ -63,13 +63,13 @@

Source: screens/unit/getDimensionColor.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_unit_instructions_ChoiceImageInstructions.js.html b/docs/api/app/screens_unit_instructions_ChoiceImageInstructions.js.html new file mode 100644 index 00000000..d1de2314 --- /dev/null +++ b/docs/api/app/screens_unit_instructions_ChoiceImageInstructions.js.html @@ -0,0 +1,206 @@ + + + + + JSDoc: Source: screens/unit/instructions/ChoiceImageInstructions.js + + + + + + + + + + +
+ +

Source: screens/unit/instructions/ChoiceImageInstructions.js

+ + + + + + +
+
+
import React, { useCallback, useRef, useState } from 'react'
+import { Animated, PixelRatio, Pressable } from 'react-native'
+import { Svg, G, Path } from 'react-native-svg'
+import { createStyleSheet } from '../../../styles/createStyleSheet'
+
+const defaultPosition = { x: 0, y: 0 }
+
+/**
+ *
+ * @param props
+ * @return {Element}
+ * @constructor
+ */
+export const ChoiceImageInstructions = props => {
+  const handPosition = useRef(new Animated.ValueXY(defaultPosition)).current
+  const handAnimation = useRef({ animation: null, running: false })
+  const [selected, setSelected] = useState(false)
+  const [size, setSize] = useState(0)
+
+  const onContainerLayout = event => {
+    const { width } = event.nativeEvent.layout
+    setSize(PixelRatio.roundToNearestPixel(width / 5))
+  }
+
+  const runAnimation = useCallback(() => {
+    if (handAnimation.current.running === false) {
+      return
+    }
+    const anim = handAnimation.current.animation ?? Animated.timing(handPosition, {
+      toValue: { x: props.width / 2 - 30, y: 0 },
+      duration: 1000,
+      useNativeDriver: false
+    })
+
+    if (handAnimation.current.animation === null) {
+      handAnimation.current.animation = anim
+    }
+
+    anim.start(() => {
+      setSelected(true)
+      anim.reset()
+      setTimeout(() => {
+        endAnimation()
+      }, 750)
+    })
+  }, [])
+
+  const endAnimation = () => {
+    handAnimation.current.running = false
+    handAnimation.current.animation.stop()
+    handAnimation.current.animation.reset()
+    setSelected(false)
+  }
+
+  const toggleAnimation = () => {
+    if (handAnimation.current.running) {
+      endAnimation()
+    }
+    else {
+      handAnimation.current.running = true
+      runAnimation()
+    }
+    if (props.onPress) {
+      props.onPress({ running: handAnimation.current.running })
+    }
+  }
+
+  return (
+    <Pressable
+      onLayout={onContainerLayout}
+      accessibilityRole='button'
+      onPress={toggleAnimation}
+      style={styles.container}
+    >
+      <Animated.View
+        style={[
+          handPosition.getLayout(),
+          styles.svgContainer
+        ]}
+        direction='alternate'
+        easing='linear'
+        iterationCount='infinite'
+        useNativeDriver
+      >
+        <HandMove width={size} height={size} />
+      </Animated.View>
+      <Animated.View
+        style={[
+          {
+            left: props.width / 2 - 30,
+            top: 0
+          },
+          styles.svgContainer
+        ]}
+        direction='alternate'
+        easing='linear'
+        iterationCount='infinite'
+        useNativeDriver
+      >
+        <ColorListImg width={size} height={size} selected={selected} />
+      </Animated.View>
+    </Pressable>
+  )
+}
+
+const ColorListImg = props => {
+  return (
+    <Svg width={props.width} height={props.height} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 46.58 52.34'>
+      <G id='Ebene_2' data-name='Ebene 2'>
+        <G id='Ebene_1-2' data-name='Ebene 1'>
+          <Path id='Pfad_163' data-name='Pfad 163' fill={props.selected ? '#5bb984' : '#183b5d'} d='M16.18,0H.85C.38,0,0,1.76,0,3.94v7.87c0,2.18.38,3.94.85,3.94H16.18c.47,0,.85-1.76.85-3.94V3.94C17,1.76,16.65,0,16.18,0Z' />
+          <Path id='Pfad_164' data-name='Pfad 164' fill='#183b5d' d='M45.73,0H30.41c-.47,0-.85,1.76-.85,3.94v7.87c0,2.18.38,3.94.85,3.94H45.73c.47,0,.85-1.76.85-3.94V3.94C46.58,1.76,46.2,0,45.73,0Z' />
+          <Path id='Pfad_165' data-name='Pfad 165' fill='#183b5d' d='M16.18,28.33H.85C.38,28.33,0,30.1,0,32.27v7.88c0,2.17.38,3.93.85,3.93H16.18c.47,0,.85-1.76.85-3.93V32.27C17,30.1,16.65,28.33,16.18,28.33Z' />
+          <Path id='Pfad_166' data-name='Pfad 166' fill='#183b5d' d='M45.73,28.33H30.41c-.47,0-.85,1.77-.85,3.94v7.88c0,2.17.38,3.93.85,3.93H45.73c.47,0,.85-1.76.85-3.93V32.27C46.58,30.1,46.2,28.33,45.73,28.33Z' />
+          <Path id='Pfad_167' data-name='Pfad 167' fill='#183b5d' d='M38.09,17.5A3.33,3.33,0,0,0,38,24.16h.12a3.33,3.33,0,0,0,0-6.66Z' />
+          <Path id='Pfad_168' data-name='Pfad 168' fill={props.selected ? '#5bb984' : '#183b5d'} d='M8.24,17.5a3.33,3.33,0,1,0-.11,6.66h.11a3.33,3.33,0,1,0,.12-6.66Z' />
+          <Path id='Pfad_169' data-name='Pfad 169' fill='#183b5d' d='M38.09,45.68A3.33,3.33,0,0,0,38,52.34h.12a3.33,3.33,0,0,0,0-6.66Z' />
+          <Path id='Pfad_170' data-name='Pfad 170' fill='#183b5d' d='M8.24,45.68a3.33,3.33,0,1,0-.11,6.66h.11a3.33,3.33,0,1,0,.12-6.66Z' />
+        </G>
+      </G>
+    </Svg>
+  )
+}
+
+const HandMove = props => {
+  return (
+    <Svg width={props.width} height={props.height} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 36.9 32.14'>
+      <G id='Ebene_2' data-name='Ebene 2'>
+        <G id='Ebene_1-2' data-name='Ebene 1'>
+          <G id='Gruppe_282' data-name='Gruppe 282'>
+            <Path
+              id='Pfad_171-4' data-name='Pfad 171-4' fill='#183b5d'
+              d='M32.43,2.28a3.48,3.48,0,0,1,4.31,2,3.42,3.42,0,0,1-2.1,4.26l-7.15,2.51a3.74,3.74,0,0,1,1.42,5.08l-.09.16a3.47,3.47,0,0,1,.39,4.87c1.88,3.29.22,5.65-3.41,6.93l-1.15.39c-4.43,1.57-6.28-.29-9.82.36a1.81,1.81,0,0,1-2-1.19L8.48,15.39h0a3.63,3.63,0,0,1,.93-3.86c1.74-1.65,5.6-5.91,5.75-8.24A3.23,3.23,0,0,1,17.3.21a3.63,3.63,0,0,1,4.64,2.23,3.75,3.75,0,0,1,.2,1.45A11,11,0,0,1,21.75,6L32.43,2.27ZM7,14.77,11.8,28.51a1.83,1.83,0,0,1-1.12,2.32L7.25,32a1.82,1.82,0,0,1-2.32-1.12L.1,17.18a1.83,1.83,0,0,1,1.12-2.32l3.43-1.21A1.82,1.82,0,0,1,7,14.77ZM9.19,27.5a1.52,1.52,0,1,0-.93,1.93h0a1.51,1.51,0,0,0,.93-1.93Z'
+            />
+          </G>
+        </G>
+      </G>
+    </Svg>
+  )
+}
+
+const styles = createStyleSheet({
+  container: {
+    borderColor: '#00f',
+    flexDirection: 'row',
+    flexGrow: 1,
+    alignItems: 'flex-start',
+    justifyContent: 'flex-start'
+  },
+  svgContainer: {
+    flex: 0,
+    justifyContent: 'center',
+    alignSelf: 'center'
+  }
+})
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time) +
+ + + + + diff --git a/docs/api/app/screens_unit_renderer_ContentRenderer.js.html b/docs/api/app/screens_unit_renderer_ContentRenderer.js.html index 7b1a028f..dfb6aeee 100644 --- a/docs/api/app/screens_unit_renderer_ContentRenderer.js.html +++ b/docs/api/app/screens_unit_renderer_ContentRenderer.js.html @@ -98,13 +98,13 @@

Source: screens/unit/renderer/ContentRenderer.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_unit_renderer_InstructionsGraphicsRenderer.js.html b/docs/api/app/screens_unit_renderer_InstructionsGraphicsRenderer.js.html index 11d93a94..d11a15cf 100644 --- a/docs/api/app/screens_unit_renderer_InstructionsGraphicsRenderer.js.html +++ b/docs/api/app/screens_unit_renderer_InstructionsGraphicsRenderer.js.html @@ -26,10 +26,10 @@

Source: screens/unit/renderer/InstructionsGraphicsRendere
-
import React, { useEffect, useState } from 'react'
-import { View } from 'react-native'
+            
import React, { useCallback, useEffect, useState } from 'react'
+import { Vibration, View } from 'react-native'
 import { InstructionAnimations } from '../instructions/InstructionAnimations'
-import { useTts } from '../../../components/Tts'
+import { useTts, TTSengine } from '../../../components/Tts'
 import { createStyleSheet } from '../../../styles/createStyleSheet'
 import { Loading } from '../../../components/Loading'
 
@@ -58,6 +58,11 @@ 

Source: screens/unit/renderer/InstructionsGraphicsRendere return () => clearTimeout(timer) }, [text, subtype, page]) + const onInstructionPress = useCallback(() => { + Vibration.vibrate(100) + TTSengine.speakImmediately(text) + }, [text]) + if (loading) { return ( <View style={styles.loadContainer}> @@ -77,7 +82,9 @@

Source: screens/unit/renderer/InstructionsGraphicsRendere return ( <View style={styles.container}> <Tts ttsText={text} color={color} dontShowText /> - <Instruction color={color} height={100} width={200} /> + <View style={styles.instructionWrapper}> + <Instruction color={color} height={100} width={200} onPress={onInstructionPress} /> + </View> </View> ) } @@ -97,6 +104,12 @@

Source: screens/unit/renderer/InstructionsGraphicsRendere tts: { margin: 0, padding: 0 + }, + instructionWrapper: { + flex: 1, + paddingLeft: 20, + paddingTop: 15, + paddingBottom: 15 } })

@@ -109,13 +122,13 @@

Source: screens/unit/renderer/InstructionsGraphicsRendere
- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_unit_renderer_UnitRenderer.js.html b/docs/api/app/screens_unit_renderer_UnitRenderer.js.html index 860ee598..bddc845d 100644 --- a/docs/api/app/screens_unit_renderer_UnitRenderer.js.html +++ b/docs/api/app/screens_unit_renderer_UnitRenderer.js.html @@ -27,16 +27,16 @@

Source: screens/unit/renderer/UnitRenderer.js

import React, { useRef, useEffect, useState } from 'react'
-import { ScrollView, Vibration, View } from 'react-native'
+import { KeyboardAvoidingView, ScrollView, Vibration, View } from 'react-native'
 import { FadePanel } from '../../../components/FadePanel'
 import { mergeStyles } from '../../../styles/mergeStyles'
 import { LeaText } from '../../../components/LeaText'
-import { Icon } from 'react-native-elements'
+import Icon from '@expo/vector-icons/FontAwesome6'
 import { Colors } from '../../../constants/Colors'
 import { createStyleSheet } from '../../../styles/createStyleSheet'
 import { InstructionsGraphicsRenderer } from './InstructionsGraphicsRenderer'
 import { useTranslation } from 'react-i18next'
-import { useTts } from '../../../components/Tts'
+import { TTSengine, useTts } from '../../../components/Tts'
 import { Layout } from '../../../constants/Layout'
 import { useKeyboardVisibilityHandler } from '../../../hooks/useKeyboardVisibilityHandler'
 import { Sound } from '../../../env/Sound'
@@ -44,9 +44,10 @@ 

Source: screens/unit/renderer/UnitRenderer.js

import { ContentRenderer } from './ContentRenderer' import { useItemSubType } from '../useItemSubType' import { Log } from '../../../infrastructure/Log' +import { isIOS } from '../../../utils/isIOS' const PureContentRenderer = React.memo(ContentRenderer) - +const debug = Log.create('UnitRenderer', 'debug') /** * Renders the Unit, independent of the surrounding * environment. @@ -84,6 +85,8 @@

Source: screens/unit/renderer/UnitRenderer.js

// We need to know the Keyboard state in order to show or hide elements. // For example: In "editing" mode of a writing item we want to hide the "check" button. useKeyboardVisibilityHandler(({ status }) => { + debug('keyboard visibility changed', status) + if (status === 'shown') { setKeyboardStatus('shown') } @@ -127,10 +130,12 @@

Source: screens/unit/renderer/UnitRenderer.js

if (allTrue) { Vibration.vibrate(500) scrollViewRef.current?.scrollToEnd({ animated: true }) + TTSengine.stop() await Sound.play(RIGHT_ANSWER) } else { Vibration.vibrate(100) + TTSengine.stop() await Sound.play(WRONG_ANSWER) } } @@ -154,7 +159,6 @@

Source: screens/unit/renderer/UnitRenderer.js

color={Colors.gray} size={10} name='info' - type='font-awesome-5' /> </View> <InstructionsGraphicsRenderer @@ -181,7 +185,6 @@

Source: screens/unit/renderer/UnitRenderer.js

color={Colors.success} size={20} name='thumbs-up' - type='font-awesome-5' /> </View> ) @@ -199,54 +202,59 @@

Source: screens/unit/renderer/UnitRenderer.js

} return ( - <ScrollView - ref={scrollViewRef} - onMomentumScrollEnd={updateLastScrollPos} - contentContainerStyle={styles.scrollView} - persistentScrollbar - keyboardShouldPersistTaps='always' - > - {/* 1. PART STIMULI */} - <FadePanel style={mergeStyles(unitCardStyles, dropShadow)} visible={fadeIn >= 0}> - <PureContentRenderer - elements={unitDoc.stimuli} - keyPrefix={`${unitId}-stimuli`} - dimensionColor={dimensionColor} - /> - </FadePanel> - - {/* 2. PART INSTRUCTIONS */} - {renderInstructions()} - - {/* 3. PART TASK PAGE CONTENT */} - <FadePanel - style={{ ...unitCardStyles, borderWidth: 3, borderColor: Colors.gray, paddingTop: 0, paddingBottom: 20 }} - visible={fadeIn >= 2} + <KeyboardAvoidingView keyboardVerticalOffset={50} behavior={isIOS() ? 'padding' : 'position'}> + <ScrollView + ref={scrollViewRef} + onMomentumScrollEnd={updateLastScrollPos} + contentContainerStyle={styles.scrollView} + persistentScrollbar + keyboardDismissMode='none' + contentInset={{ bottom: 20 }} + keyboardShouldPersistTaps='always' + automaticallyAdjustKeyboardInsets > - <LeaText style={styles.pageText}>{page + 1} / {unitDoc.pages.length}</LeaText> - - <PureContentRenderer - elements={unitDoc.pages[page]?.content} - keyPrefix={`${unitId}-${page}`} - scoreResult={showCorrectResponse && scoreResult} - showCorrectResponse={showCorrectResponse} - dimensionColor={dimensionColor} - submitResponse={submitResponse} - /> - </FadePanel> + {/* 1. PART STIMULI */} + <FadePanel style={mergeStyles(unitCardStyles, dropShadow)} visible={fadeIn >= 0}> + <PureContentRenderer + elements={unitDoc.stimuli} + keyPrefix={`${unitId}-stimuli`} + dimensionColor={dimensionColor} + /> + </FadePanel> + + {/* 2. PART INSTRUCTIONS */} + {renderInstructions()} + + {/* 3. PART TASK PAGE CONTENT */} + <FadePanel + style={{ ...unitCardStyles, borderWidth: 3, borderColor: Colors.gray, paddingTop: 0, paddingBottom: 20 }} + visible={fadeIn >= 2} + > + <LeaText style={styles.pageText}>{page + 1} / {unitDoc.pages.length}</LeaText> + + <PureContentRenderer + elements={unitDoc.pages[page]?.content} + keyPrefix={`${unitId}-${page}`} + scoreResult={showCorrectResponse && scoreResult} + showCorrectResponse={showCorrectResponse} + dimensionColor={dimensionColor} + submitResponse={submitResponse} + /> + </FadePanel> - {renderAllTrue()} + {renderAllTrue()} - {renderFooter()} - </ScrollView> + {renderFooter()} + </ScrollView> + </KeyboardAvoidingView> ) } const RIGHT_ANSWER = 'rightAnswer' const WRONG_ANSWER = 'wrongAnswer' -Sound.load(RIGHT_ANSWER, () => require('../../../assets/audio/right_answer.wav')) -Sound.load(WRONG_ANSWER, () => require('../../../assets/audio/wrong_answer.mp3')) +Sound.load(RIGHT_ANSWER, () => require('../../../../assets/audio/right_answer.wav')) +Sound.load(WRONG_ANSWER, () => require('../../../../assets/audio/wrong_answer.mp3')) const styles = createStyleSheet({ instructionStyles: { @@ -301,13 +309,13 @@

Source: screens/unit/renderer/UnitRenderer.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_unit_renderer_UnitSetRenderer.js.html b/docs/api/app/screens_unit_renderer_UnitSetRenderer.js.html index ba1e6bad..c5d7fb36 100644 --- a/docs/api/app/screens_unit_renderer_UnitSetRenderer.js.html +++ b/docs/api/app/screens_unit_renderer_UnitSetRenderer.js.html @@ -73,13 +73,13 @@

Source: screens/unit/renderer/UnitSetRenderer.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/screens_unit_shouldRenderStory.js.html b/docs/api/app/screens_unit_shouldRenderStory.js.html index 166a4e5c..1aea01c8 100644 --- a/docs/api/app/screens_unit_shouldRenderStory.js.html +++ b/docs/api/app/screens_unit_shouldRenderStory.js.html @@ -57,13 +57,13 @@

Source: screens/unit/shouldRenderStory.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/state_AppSession.js.html b/docs/api/app/state_AppSession.js.html index c03cb8ed..96f836e0 100644 --- a/docs/api/app/state_AppSession.js.html +++ b/docs/api/app/state_AppSession.js.html @@ -144,13 +144,13 @@

Source: state/AppSession.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/state_createStorageAPI.js.html b/docs/api/app/state_createStorageAPI.js.html index f37b0adc..7082e995 100644 --- a/docs/api/app/state_createStorageAPI.js.html +++ b/docs/api/app/state_createStorageAPI.js.html @@ -112,13 +112,13 @@

Source: state/createStorageAPI.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/styles_createStyleSheet.js.html b/docs/api/app/styles_createStyleSheet.js.html index 0e50f2ec..f2c92940 100644 --- a/docs/api/app/styles_createStyleSheet.js.html +++ b/docs/api/app/styles_createStyleSheet.js.html @@ -60,13 +60,13 @@

Source: styles/createStyleSheet.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/tts_TTSVoiceConfig.js.html b/docs/api/app/tts_TTSVoiceConfig.js.html index 5761956c..b3e8aed2 100644 --- a/docs/api/app/tts_TTSVoiceConfig.js.html +++ b/docs/api/app/tts_TTSVoiceConfig.js.html @@ -26,17 +26,19 @@

Source: tts/TTSVoiceConfig.js

-
import React, { useState } from 'react'
+            
import React, { useEffect, useState } from 'react'
 import { useVoices } from '../hooks/useVoices'
 import { View } from 'react-native'
 import { Loading } from '../components/Loading'
 import { createStyleSheet } from '../styles/createStyleSheet'
 import { Layout } from '../constants/Layout'
-import { LeaButtonGroup } from '../components/LeaButtonGroup'
 import { TTSengine } from '../components/Tts'
 import { useTranslation } from 'react-i18next'
 import { InteractionGraph } from '../infrastructure/log/InteractionGraph'
 import { isIOS } from '../utils/isIOS'
+import { LeaButton } from '../components/LeaButton'
+import { Colors } from '../constants/Colors'
+import { mergeStyles } from '../styles/mergeStyles'
 
 /**
  * Allows to set the voice for tts.
@@ -49,7 +51,17 @@ 

Source: tts/TTSVoiceConfig.js

export const TTSVoiceConfig = props => { const { t } = useTranslation() const { voices, voicesLoaded, currentVoice } = useVoices() - const [selected, setSelected] = useState(false) + const [selected, setSelected] = useState(-1) + + // set a default voice + useEffect(() => { + if (selected === -1) { + const index = voices.findIndex(v => v.identifier === currentVoice) + if (index > -1) { + setSelected(index) + } + } + }, [voices, currentVoice]) if (!voicesLoaded) { return ( @@ -66,15 +78,15 @@

Source: tts/TTSVoiceConfig.js

} const justNumbers = voices.length > 3 - const handleChange = (_, index) => { + const handleChange = (index) => { const voice = voices[index] const text = isIOS() ? voice.name : getName({ voice, index, justNumbers: false, t }) setNewVoice({ voice, text }) - if (!selected) { - setSelected(true) + if (selected !== index) { + setSelected(index) } if (props.onChange) { @@ -82,24 +94,29 @@

Source: tts/TTSVoiceConfig.js

} } - const groupData = voices.map((voice, index) => { - return getName({ voice, index, justNumbers, t }) - }) - // if we haven't selected anything yet but // there is an initial set voice // we set its index to being active - const activeIndex = selected - ? null - : voices.findIndex(voice => voice.identifier === currentVoice) - return ( - <LeaButtonGroup - active={activeIndex} - data={groupData} - style={props.style} - onPress={handleChange} - /> + <View style={props.style}> + {voices.map((voice, index) => { + const name = getName({ voice, index, justNumbers, t }) + const buttonStyle = { + backgroundColor: index === selected ? Colors.primary : Colors.white + } + return ( + <View style={styles.container} key={index}> + <LeaButton + title={name} + onPress={() => handleChange(index)} + icon='user' + color={index === selected ? Colors.white : Colors.primary} + buttonStyle={mergeStyles(styles.entry, buttonStyle)} + /> + </View> + ) + })} + </View> ) } @@ -120,9 +137,11 @@

Source: tts/TTSVoiceConfig.js

} const value = index + 1 + const plain = t('tts.voice', { value }) + const name = justNumbers - ? String(value) - : t('tts.voice', { value }) + ? plain + : t('tts.hello', { name: plain }) return name } @@ -148,7 +167,16 @@

Source: tts/TTSVoiceConfig.js

} const styles = createStyleSheet({ - container: Layout.container() + container: { + ...Layout.container(), + margin: 20 + }, + row: { + flex: 1 + }, + entry: { + padding: 20 + } })
@@ -160,13 +188,13 @@

Source: tts/TTSVoiceConfig.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/utils_array_byDocId.js.html b/docs/api/app/utils_array_byDocId.js.html index ffcdce54..d5c5a329 100644 --- a/docs/api/app/utils_array_byDocId.js.html +++ b/docs/api/app/utils_array_byDocId.js.html @@ -43,13 +43,13 @@

Source: utils/array/byDocId.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/utils_array_byOrderedIds.js.html b/docs/api/app/utils_array_byOrderedIds.js.html index ad33ba6b..630c619b 100644 --- a/docs/api/app/utils_array_byOrderedIds.js.html +++ b/docs/api/app/utils_array_byOrderedIds.js.html @@ -53,13 +53,13 @@

Source: utils/array/byOrderedIds.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/utils_array_randomArrayElement.js.html b/docs/api/app/utils_array_randomArrayElement.js.html index d92bdd69..ea44b6a2 100644 --- a/docs/api/app/utils_array_randomArrayElement.js.html +++ b/docs/api/app/utils_array_randomArrayElement.js.html @@ -58,13 +58,13 @@

Source: utils/array/randomArrayElement.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/utils_array_toArrayIfNot.js.html b/docs/api/app/utils_array_toArrayIfNot.js.html index bf19fff9..3a2ad3f0 100644 --- a/docs/api/app/utils_array_toArrayIfNot.js.html +++ b/docs/api/app/utils_array_toArrayIfNot.js.html @@ -50,13 +50,13 @@

Source: utils/array/toArrayIfNot.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/utils_array_toDocId.js.html b/docs/api/app/utils_array_toDocId.js.html index 80e0cf72..cdbc4f29 100644 --- a/docs/api/app/utils_array_toDocId.js.html +++ b/docs/api/app/utils_array_toDocId.js.html @@ -42,13 +42,13 @@

Source: utils/array/toDocId.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/utils_asyncTimeout.js.html b/docs/api/app/utils_asyncTimeout.js.html index b4323d74..474eab2b 100644 --- a/docs/api/app/utils_asyncTimeout.js.html +++ b/docs/api/app/utils_asyncTimeout.js.html @@ -48,13 +48,13 @@

Source: utils/asyncTimeout.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/utils_createTimedPromise.js.html b/docs/api/app/utils_createTimedPromise.js.html index 8ffcced9..79b5f820 100644 --- a/docs/api/app/utils_createTimedPromise.js.html +++ b/docs/api/app/utils_createTimedPromise.js.html @@ -47,7 +47,7 @@

Source: utils/createTimedPromise.js

* @param details {*=} optional any detail attached to the error context for better error tracing * @return {Promise<Awaited<unknown>>} */ -export const createTimedPromise = (promise, { timeout = 1000, throwIfTimedOut = false, message, details } = {}) => { +export const createTimedPromise = (promise, { timeout = 5000, throwIfTimedOut = false, message, details } = {}) => { let timeOut const race = Promise.race([ @@ -81,13 +81,13 @@

Source: utils/createTimedPromise.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/utils_math_randomIntInclusive.js.html b/docs/api/app/utils_math_randomIntInclusive.js.html index 9b2d0e56..1a69bc6e 100644 --- a/docs/api/app/utils_math_randomIntInclusive.js.html +++ b/docs/api/app/utils_math_randomIntInclusive.js.html @@ -53,13 +53,13 @@

Source: utils/math/randomIntInclusive.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/utils_number_isSafeInteger.js.html b/docs/api/app/utils_number_isSafeInteger.js.html index c5b084bb..e5cb710a 100644 --- a/docs/api/app/utils_number_isSafeInteger.js.html +++ b/docs/api/app/utils_number_isSafeInteger.js.html @@ -48,13 +48,13 @@

Source: utils/number/isSafeInteger.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/utils_number_isValidNumber.js.html b/docs/api/app/utils_number_isValidNumber.js.html index 019ff958..288c4e43 100644 --- a/docs/api/app/utils_number_isValidNumber.js.html +++ b/docs/api/app/utils_number_isValidNumber.js.html @@ -55,13 +55,13 @@

Source: utils/number/isValidNumber.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/utils_number_toInteger.js.html b/docs/api/app/utils_number_toInteger.js.html index a8d515a5..978feb25 100644 --- a/docs/api/app/utils_number_toInteger.js.html +++ b/docs/api/app/utils_number_toInteger.js.html @@ -44,13 +44,13 @@

Source: utils/number/toInteger.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/utils_number_toPrecisionNumber.js.html b/docs/api/app/utils_number_toPrecisionNumber.js.html index 0c67fb59..5706f33c 100644 --- a/docs/api/app/utils_number_toPrecisionNumber.js.html +++ b/docs/api/app/utils_number_toPrecisionNumber.js.html @@ -45,13 +45,13 @@

Source: utils/number/toPrecisionNumber.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/utils_object_clearObject.js.html b/docs/api/app/utils_object_clearObject.js.html index 89e40bb4..9f772bbe 100644 --- a/docs/api/app/utils_object_clearObject.js.html +++ b/docs/api/app/utils_object_clearObject.js.html @@ -55,13 +55,13 @@

Source: utils/object/clearObject.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/utils_object_hasOwnProp.js.html b/docs/api/app/utils_object_hasOwnProp.js.html index 7fbc5b49..8cc7bdbb 100644 --- a/docs/api/app/utils_object_hasOwnProp.js.html +++ b/docs/api/app/utils_object_hasOwnProp.js.html @@ -43,13 +43,13 @@

Source: utils/object/hasOwnProp.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/utils_object_isDefined.js.html b/docs/api/app/utils_object_isDefined.js.html index f80af8d6..e223fe85 100644 --- a/docs/api/app/utils_object_isDefined.js.html +++ b/docs/api/app/utils_object_isDefined.js.html @@ -42,13 +42,13 @@

Source: utils/object/isDefined.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/utils_simpleRandomHex.js.html b/docs/api/app/utils_simpleRandomHex.js.html index ba8fa0c3..42c14e85 100644 --- a/docs/api/app/utils_simpleRandomHex.js.html +++ b/docs/api/app/utils_simpleRandomHex.js.html @@ -47,13 +47,13 @@

Source: utils/simpleRandomHex.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/utils_text_createSimpleTokenizer.js.html b/docs/api/app/utils_text_createSimpleTokenizer.js.html index b95969b9..cdd10bc0 100644 --- a/docs/api/app/utils_text_createSimpleTokenizer.js.html +++ b/docs/api/app/utils_text_createSimpleTokenizer.js.html @@ -82,13 +82,13 @@

Source: utils/text/createSimpleTokenizer.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/utils_text_isWord.js.html b/docs/api/app/utils_text_isWord.js.html index bc1ad7ab..7093d557 100644 --- a/docs/api/app/utils_text_isWord.js.html +++ b/docs/api/app/utils_text_isWord.js.html @@ -42,13 +42,13 @@

Source: utils/text/isWord.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
diff --git a/docs/api/app/utils_trigonometry_getPositionOnCircle.js.html b/docs/api/app/utils_trigonometry_getPositionOnCircle.js.html index 62199b9d..d819c2fa 100644 --- a/docs/api/app/utils_trigonometry_getPositionOnCircle.js.html +++ b/docs/api/app/utils_trigonometry_getPositionOnCircle.js.html @@ -63,13 +63,13 @@

Source: utils/trigonometry/getPositionOnCircle.js


- Documentation generated by JSDoc 4.0.2 on Wed Oct 25 2023 09:40:55 GMT+0200 (Central European Summer Time) + Documentation generated by JSDoc 4.0.3 on Tue Sep 03 2024 11:47:20 GMT+0200 (Central European Summer Time)
From 360266d2998911e9ec0baf7d9cbac460d2ae664a Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Tue, 3 Sep 2024 11:49:17 +0200 Subject: [PATCH 3/4] fix(app): standard js lint fix --- app/__tests__/contexts/MapIcons.tests.js | 2 +- app/__tests__/contexts/Sync.tests.js | 2 +- app/__tests__/i18n/i18n.test.js | 2 +- app/__tests__/screens/map/loadMapData.tests.js | 3 --- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/app/__tests__/contexts/MapIcons.tests.js b/app/__tests__/contexts/MapIcons.tests.js index 9d35f410..99b36539 100644 --- a/app/__tests__/contexts/MapIcons.tests.js +++ b/app/__tests__/contexts/MapIcons.tests.js @@ -1,7 +1,7 @@ import { MapIcons } from '../../lib/contexts/MapIcons' import { createContextBaseTests } from '../../__testHelpers__/createContextBaseTests' import { simpleRandomHex } from '../../lib/utils/simpleRandomHex' -import { render } from '@testing-library/react-native'; +import { render } from '@testing-library/react-native' describe(MapIcons.name, () => { createContextBaseTests({ ctx: MapIcons }) diff --git a/app/__tests__/contexts/Sync.tests.js b/app/__tests__/contexts/Sync.tests.js index a359e515..80f84dad 100644 --- a/app/__tests__/contexts/Sync.tests.js +++ b/app/__tests__/contexts/Sync.tests.js @@ -2,7 +2,7 @@ import Meteor from '@meteorrn/core' import { Sync } from '../../lib/infrastructure/sync/Sync' import { simpleRandom } from '../../__testHelpers__/simpleRandom' import { collectionExists } from '../../lib/infrastructure/collections/collections' -import { restoreAll, stub, overrideStub } from '../../__testHelpers__/stub' +import { restoreAll, stub } from '../../__testHelpers__/stub' import { createContextStorage } from '../../lib/contexts/createContextStorage' import { cleanup } from '@testing-library/react-native' import { ContextRepository } from '../../lib/infrastructure/ContextRepository' diff --git a/app/__tests__/i18n/i18n.test.js b/app/__tests__/i18n/i18n.test.js index c7c7b282..ca985b77 100644 --- a/app/__tests__/i18n/i18n.test.js +++ b/app/__tests__/i18n/i18n.test.js @@ -20,7 +20,7 @@ test('recursively iterate all object keys of i18 EN and DE, checks if the same n }) return keys } - const byName = (a,b) => a.localeCompare(b) + const byName = (a, b) => a.localeCompare(b) const deKeys = toKeys(translationDE).sort(byName) const enKeys = toKeys(translationEN).sort(byName) diff --git a/app/__tests__/screens/map/loadMapData.tests.js b/app/__tests__/screens/map/loadMapData.tests.js index 83aae763..c2ed615b 100644 --- a/app/__tests__/screens/map/loadMapData.tests.js +++ b/app/__tests__/screens/map/loadMapData.tests.js @@ -1,4 +1,3 @@ -import Meteor from '@meteorrn/core' import { Dimension } from '../../../lib/contexts/Dimension' import { stub, restoreAll } from '../../../__testHelpers__/stub' import { simpleRandom } from '../../../__testHelpers__/simpleRandom' @@ -32,8 +31,6 @@ describe(loadMapData.name, () => { it('returns an "empty" message if the server responded with no or faulty map data', async () => { const fieldDoc = { _id: simpleRandom() } - - const allData = [ undefined, null, From 9873a404b7a3246d057dc4f3037c2ef9c728b7b1 Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Tue, 3 Sep 2024 12:13:03 +0200 Subject: [PATCH 4/4] ci: updated workflows --- .github/workflows/Audit.yml | 34 ---------------- .github/workflows/app.yml | 66 ++++++++++++++++++++++++++++++ .github/workflows/backend.yml | 69 ++++++++++++++++++-------------- .github/workflows/jest_test.yml | 33 --------------- .github/workflows/jsdoc_test.yml | 31 -------------- .github/workflows/lint_test.yml | 31 -------------- 6 files changed, 106 insertions(+), 158 deletions(-) delete mode 100644 .github/workflows/Audit.yml create mode 100644 .github/workflows/app.yml delete mode 100644 .github/workflows/jest_test.yml delete mode 100644 .github/workflows/jsdoc_test.yml delete mode 100644 .github/workflows/lint_test.yml diff --git a/.github/workflows/Audit.yml b/.github/workflows/Audit.yml deleted file mode 100644 index c01bb1c1..00000000 --- a/.github/workflows/Audit.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Check for vulnerabilities - -on: - push: - branches: - - release - - pull_request: - branches: - - release - - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@v3 - - name: setup node - uses: actions/setup-node@v3 - with: - node-version: '16.x' - - name: cache dependencies - uses: actions/cache@v1 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - - name: open src folder and install node modules - run: cd src && npm install - - name: open src folder and check for vulnerabilities - run: cd src && npm audit --production diff --git a/.github/workflows/app.yml b/.github/workflows/app.yml new file mode 100644 index 00000000..fddc5cb2 --- /dev/null +++ b/.github/workflows/app.yml @@ -0,0 +1,66 @@ +name: App Testsuite + +on: + push: + branches: + - main + pull_request: + +jobs: + lint: + name: 'App Lint' + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: setup node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + cache-dependency-path: 'package-lock.json' + + - name: install node modules + run: cd app && npm i --legacy-peer-deps --force + - name: run standard js + run: cd app && npm run lint + + tests: + name: 'App Unit Tests' + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: setup node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + cache-dependency-path: 'package-lock.json' + + - name: install node modules + run: cd app && npm ci --legacy-peer-deps --force + + - name: run jest tests + run: cd app && npm test + + docs: + name: 'Build App jsDoc' + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: setup node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + cache-dependency-path: 'package-lock.json' + + - name: install node modules + run: cd app && npm i --legacy-peer-deps --force + - name: run jsdoc + run: cd app && npm run docs diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index a60139df..d51e75a6 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -3,7 +3,7 @@ name: Backend Tests on: push: branches: - - ma + - main pull_request: jobs: @@ -11,41 +11,35 @@ jobs: name: Backend JS lint runs-on: ubuntu-latest steps: - - name: checkout - uses: actions/checkout@v3 - - - name: setup node - uses: actions/setup-node@v3 - with: - node-version: '14.x' + - name: checkout + uses: actions/checkout@v4 - - name: cache dependencies - uses: actions/cache@v3 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- + - name: setup node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + cache-dependency-path: 'package-lock.json' - - run: cd backend && npm install - - run: cd backend && npm run lint:code + - run: cd backend && npm install + - run: cd backend && npm run lint:code tests: name: Backend Meteor ${{ matrix.meteor }} tests runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Checkout leaonline:corelib repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: leaonline/corelib path: github/corelib - name: Checkout leaonline:service-registry repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: leaonline/service-registry path: github/service-registry @@ -53,12 +47,12 @@ jobs: # CACHING - name: Install Meteor id: cache-meteor-install - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.meteor - key: v2-meteor-${{ hashFiles('.meteor/versions') }} + key: v3-meteor-${{ hashFiles('.meteor/versions') }} restore-keys: | - v2-meteor- + v3-meteor- - name: Cache NPM dependencies id: cache-meteor-npm @@ -67,25 +61,25 @@ jobs: path: ~/.npm key: v1-npm-${{ hashFiles('package-lock.json') }} restore-keys: | - v1-npm- + v1-npm- - name: Cache Meteor build id: cache-meteor-build - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | .meteor/local/resolver-result-cache.json .meteor/local/plugin-cache .meteor/local/isopacks .meteor/local/bundler-cache/scanner - key: v2-meteor_build_cache-${{ github.ref }}-${{ github.sha }} + key: v3-meteor_build_cache-${{ github.ref }}-${{ github.sha }} restore-key: | - v2-meteor_build_cache- + v3-meteor_build_cache- - name: Setup meteor uses: meteorengineer/setup-meteor@v1 with: - meteor-release: '2.7.3' + meteor-release: '3.0.2' - name: Install NPM Dependencies run: cd backend && meteor npm ci @@ -119,4 +113,21 @@ jobs: # uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 # with: # path: ".coverage/lcov.info" -# min_coverage: 95 # TODO increase to 95! \ No newline at end of file +# min_coverage: 95 # TODO increase to 95! + + docs: + name: Backend Build Docs + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: setup node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + cache-dependency-path: 'package-lock.json' + + - run: cd backend && npm ci + - run: cd backend && npm run build:docs diff --git a/.github/workflows/jest_test.yml b/.github/workflows/jest_test.yml deleted file mode 100644 index cbdff835..00000000 --- a/.github/workflows/jest_test.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: App Tests - - -on: - push: - branches: - - main - pull_request: - -jobs: - build: - runs-on: ubuntu-latest - steps: - - - name: checkout - uses: actions/checkout@v3 - - name: setup node - uses: actions/setup-node@v3 - with: - node-version: '16.x' - - name: cache dependencies - uses: actions/cache@v1 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - - name: install node modules - run: cd src && npm i --legacy-peer-deps --force - - - name: run jest tests - run: cd src && npm test diff --git a/.github/workflows/jsdoc_test.yml b/.github/workflows/jsdoc_test.yml deleted file mode 100644 index 94d9fe7e..00000000 --- a/.github/workflows/jsdoc_test.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: App Build Docs - -on: - push: - branches: - - main - - pull_request: - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@v3 - - name: setup node - uses: actions/setup-node@v3 - with: - node-version: '16.x' - - name: cache dependencies - uses: actions/cache@v1 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - - name: install node modules - run: cd src && npm i --legacy-peer-deps --force - - name: run jsdoc - run: cd src && npm run docs diff --git a/.github/workflows/lint_test.yml b/.github/workflows/lint_test.yml deleted file mode 100644 index 8a7c2108..00000000 --- a/.github/workflows/lint_test.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: App JS Lint - -on: - push: - branches: - - main - - pull_request: - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@v3 - - name: setup node - uses: actions/setup-node@v3 - with: - node-version: '16.x' - - name: cache dependencies - uses: actions/cache@v1 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - - name: install node modules - run: cd src && npm i --legacy-peer-deps --force - - name: run standard js - run: cd src && npm run lint