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 diff --git a/app/.eslintrc.js b/app/.eslintrc.js index 7124b993..2c0cd9c0 100644 --- a/app/.eslintrc.js +++ b/app/.eslintrc.js @@ -1,16 +1,14 @@ // https://docs.expo.dev/guides/using-eslint/ module.exports = { extends: ['expo', 'standard'], + plugins: ['jest'], + env: { + 'jest/globals': true + }, rules: { - 'react/display-name': 'off', 'react-hooks/exhaustive-deps': 'off', - 'brace-style': [ - 'error', - 'stroustrup', - { - allowSingleLine: true - } - ], + 'react/display-name': 'off', + 'brace-style': ['error', 'stroustrup', { allowSingleLine: true }], 'import/no-duplicates': 0 } } 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..99b36539 --- /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..80f84dad --- /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 } 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..ca985b77 --- /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..c2ed615b --- /dev/null +++ b/app/__tests__/screens/map/loadMapData.tests.js @@ -0,0 +1,258 @@ +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/jest.config.js b/app/jest.config.js index 087989fa..7e91d135 100644 --- a/app/jest.config.js +++ b/app/jest.config.js @@ -1,11 +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/.*)' - 'node_modules/(?!((jest-)?react-native|@meteorrn|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg)' + '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'], + coverageReporters: ['html', 'text'], setupFiles: ['./jestSetup.js'] } 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/contexts/MapIcons.js b/app/lib/contexts/MapIcons.js index b81c76b7..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,11 +50,24 @@ MapIcons.render = (index) => { const name = internal.icons[index] return ( + + ) } + +const styles = createStyleSheet({ + container: { + alignItems: 'center', + justifyContent: 'center' + }, + icon: { + flex: 0 + } +}) 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/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/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/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/package-lock.json b/app/package-lock.json index 35b1e7da..9bfa7fb0 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -58,11 +58,14 @@ "eslint-config-expo": "^7.1.2", "eslint-config-standard": "^17.1.0", "eslint-plugin-import": "^2.29.1", + "eslint-plugin-jest": "^28.8.2", "eslint-plugin-n": "^17.10.2", "eslint-plugin-promise": "^7.1.0", "jest": "^29.7.0", "jest-expo": "~51.0.3", + "jsdoc": "^4.0.3", "react-test-renderer": "18.2.0", + "sinon": "^18.0.0", "typescript": "~5.3.3" } }, @@ -3721,6 +3724,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", @@ -5972,6 +5987,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", @@ -6020,6 +6041,32 @@ "@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", @@ -6154,6 +6201,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", @@ -7258,6 +7327,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", @@ -7561,6 +7636,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", @@ -8426,6 +8513,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", @@ -8447,15 +8543,15 @@ } }, "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "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": ">=0.10.0" + "node": ">=6.0.0" } }, "node_modules/dom-serializer": { @@ -9068,9 +9164,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.2.tgz", - "integrity": "sha512-3XnC5fDyc8M4J2E8pt8pmSVRX2M+5yWMCfI/kDZwauQeFgzQOuhcRBFKjTeJagqgk4sFKxe1mvNVnaWwImx/Tg==", + "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" @@ -9131,26 +9227,27 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "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": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", + "@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.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", + "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.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", "semver": "^6.3.1", "tsconfig-paths": "^3.15.0" }, @@ -9170,6 +9267,43 @@ "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", @@ -9259,9 +9393,9 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.35.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz", - "integrity": "sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==", + "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", @@ -9302,6 +9436,18 @@ "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", @@ -9418,18 +9564,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/eslint/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/eslint/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -10499,19 +10633,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -13829,6 +13950,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", @@ -13935,6 +14065,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", @@ -14095,6 +14319,12 @@ "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", @@ -14112,6 +14342,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", @@ -14188,101 +14427,6 @@ "lightningcss-win32-x64-msvc": "1.19.0" } }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.19.0.tgz", - "integrity": "sha512-wIJmFtYX0rXHsXHSr4+sC5clwblEMji7HHQ4Ub1/CznVRxtCFha6JIt5JZaNf8vQrfdZnBxLLC6R8pC818jXqg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.19.0.tgz", - "integrity": "sha512-Lif1wD6P4poaw9c/4Uh2z+gmrWhw/HtXFoeZ3bEsv6Ia4tt8rOJBdkfVaUJ6VXmpKHALve+iTyP2+50xY1wKPw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.19.0.tgz", - "integrity": "sha512-P15VXY5682mTXaiDtbnLYQflc8BYb774j2R84FgDLJTN6Qp0ZjWEFyN1SPqyfTj2B2TFjRHRUvQSSZ7qN4Weig==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.19.0.tgz", - "integrity": "sha512-zwXRjWqpev8wqO0sv0M1aM1PpjHz6RVIsBcxKszIG83Befuh4yNysjgHVplF9RTU7eozGe3Ts7r6we1+Qkqsww==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.19.0.tgz", - "integrity": "sha512-vSCKO7SDnZaFN9zEloKSZM5/kC5gbzUjoJQ43BvUpyTFUX7ACs/mDfl2Eq6fdz2+uWhUh7vf92c4EaaP4udEtA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lightningcss-linux-x64-gnu": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.19.0.tgz", @@ -14321,25 +14465,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.19.0.tgz", - "integrity": "sha512-C+VuUTeSUOAaBZZOPT7Etn/agx/MatzJzGRkeV+zEABmPuntv1zihncsi+AyGmjkkzq3wVedEy7h0/4S84mUtg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -14377,6 +14502,12 @@ "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", @@ -14625,6 +14756,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", @@ -15422,6 +15563,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", @@ -16022,6 +16185,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", @@ -16318,6 +16487,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", @@ -17153,6 +17331,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", @@ -17611,6 +17798,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", @@ -18728,6 +18963,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", @@ -19357,6 +19598,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 17d61138..2c05f6bf 100644 --- a/app/package.json +++ b/app/package.json @@ -9,8 +9,9 @@ "ios": "expo start --ios", "web": "expo start --web", "test": "jest --coverage", - "docs": "jsdoc -c jsdoc.conf.json", + "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" }, @@ -68,8 +69,11 @@ "eslint-plugin-n": "^17.10.2", "eslint-plugin-promise": "^7.1.0", "jest": "^29.7.0", + "eslint-plugin-jest": "^28.8.2", "jest-expo": "~51.0.3", + "jsdoc": "^4.0.3", "react-test-renderer": "18.2.0", + "sinon": "^18.0.0", "typescript": "~5.3.3" }, "private": true 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)