Skip to content

Commit

Permalink
Merge pull request #57 from akvo/feature/39-cascade-question-type-test
Browse files Browse the repository at this point in the history
Feature/39 cascade question type test
  • Loading branch information
dedenbangkit authored Jul 25, 2023
2 parents 93c9162 + 9c4c975 commit 009cfeb
Show file tree
Hide file tree
Showing 30 changed files with 777 additions and 23 deletions.
2 changes: 2 additions & 0 deletions app/__mocks__/assets/example.db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module.exports =
'CREATE TABLE examples( id INT PRIMARY KEY, name VARCHAR(100) );INSERT INTO examples(id, name) values(1, "JHON")';
8 changes: 8 additions & 0 deletions app/__tests__/database.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,4 +248,12 @@ describe('conn.tx', () => {
expect.any(Function),
);
});

test('should execute query where null successfully', async () => {
const table = 'users';
const where = { name: null };
const selectWhereNull = query.read(table, where);

expect(selectWhereNull).toEqual('SELECT * FROM users WHERE name IS NULL;');
});
});
7 changes: 7 additions & 0 deletions app/metro.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const { getDefaultConfig } = require('expo/metro-config');

const defaultConfig = getDefaultConfig(__dirname);

defaultConfig.resolver.assetExts.push('db');

module.exports = defaultConfig;
Binary file added app/src/assets/administrations.db
Binary file not shown.
7 changes: 7 additions & 0 deletions app/src/components/LogoutButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ListItem, Dialog, Text, Icon } from '@rneui/themed';
import { useNavigation } from '@react-navigation/native';
import { AuthState, UserState } from '../store';
import { conn, query } from '../database';
import { cascades } from '../lib';

const db = conn.init;

Expand Down Expand Up @@ -31,6 +32,12 @@ const LogoutButton = () => {
});
setLoading(false);
setVisible(false);

/**
* Remove sqlite files
*/
await cascades.dropFiles();

navigation.navigate('GetStarted');
};

Expand Down
11 changes: 9 additions & 2 deletions app/src/components/__tests__/LogoutButton.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@ import { useNavigation } from '@react-navigation/native';
import LogoutButton from '../LogoutButton';
import { AuthState, UserState } from '../../store';
import { conn, query } from '../../database';
import { cascades } from '../../lib';

jest.mock('@react-navigation/native');
jest.mock('expo-sqlite');
jest.mock('../../lib', () => ({
cascades: {
dropFiles: jest.fn(async () => ['file.sqlite', 'file.sqlite-journal']),
},
}));

const db = conn.init;

Expand Down Expand Up @@ -95,13 +101,14 @@ describe('LogoutButton', () => {
expect(yesEl).toBeDefined();
fireEvent.press(yesEl);

act(() => {
act(async () => {
await cascades.dropFiles();
AuthState.update((s) => {
s.token = null;
});
});

await waitFor(async () => {
await waitFor(() => {
const { result } = renderHook(() => AuthState.useState());
const { token } = result.current;
expect(token).toBe(null);
Expand Down
35 changes: 35 additions & 0 deletions app/src/database/__tests__/conn.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Asset } from 'expo-asset';
import * as FileSystem from 'expo-file-system';
import * as SQLite from 'expo-sqlite';
import { act, renderHook, waitFor, fireEvent } from '@testing-library/react-native';
import { conn } from '../conn';
import exampledb from 'assets/example.db';

jest.mock('expo-asset', () => {
return {
Asset: {
fromModule: jest.fn((module) => ({
uri: `mocked-uri-for-${module}`,
})),
},
};
});

jest.mock('expo-file-system', () => {
return {
getInfoAsync: jest.fn().mockResolvedValue({ exists: false }),
makeDirectoryAsync: jest.fn(),
downloadAsync: jest.fn(),
};
});

describe('openDBFile', () => {
it('should have db connection from file', async () => {
const dbFile = exampledb;
const db = await conn.file(dbFile, 'example');

await waitFor(() => {
expect(db.transaction).toBeDefined();
});
});
});
14 changes: 14 additions & 0 deletions app/src/database/conn.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Platform } from 'react-native';
import { Asset } from 'expo-asset';
import * as FileSystem from 'expo-file-system';
import * as SQLite from 'expo-sqlite';

const openDatabase = () => {
Expand Down Expand Up @@ -50,7 +52,19 @@ const tx = (db, query, params = []) => {
});
};

const openDBfile = async (databaseFile, databaseName) => {
if (!(await FileSystem.getInfoAsync(FileSystem.documentDirectory + 'SQLite')).exists) {
await FileSystem.makeDirectoryAsync(FileSystem.documentDirectory + 'SQLite');
}
await FileSystem.downloadAsync(
Asset.fromModule(databaseFile).uri,
FileSystem.documentDirectory + `SQLite/${databaseName}.db`,
);
return SQLite.openDatabase(`${databaseName}.db`);
};

export const conn = {
file: (dbFile, dbName) => openDBfile(dbFile, dbName),
init,
tx,
};
4 changes: 3 additions & 1 deletion app/src/database/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ const update = (table, where = {}, data = {}) => {
};

const read = (table, where = {}, nocase = false) => {
const conditions = Object.keys(where).map((key) => `${key} = ?`);
const conditions = Object.keys(where).map((key) => {
return where[key] === null ? `${key} IS NULL` : `${key} = ?`;
});
let conditionString = '';
if (conditions.length) {
conditionString = `WHERE ${conditions.join(' AND ')}`;
Expand Down
1 change: 0 additions & 1 deletion app/src/form/FormContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { FormState } from '../store';

// TODO:: Allow other not supported yet
// TODO:: Repeat group not supported yet
// TODO:: Cascade not supported yet

const FormContainer = ({ forms, initialValues = {}, onSubmit }) => {
const formRef = useRef();
Expand Down
4 changes: 4 additions & 0 deletions app/src/form/__test__/FormContainer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ const exampleTestForm = {
],
};

jest.mock('../../assets/administrations.db', () => {
return 'data';
});

describe('FormContainer component', () => {
test('submits form data correctly without dependency', async () => {
const handleOnSubmit = jest.fn();
Expand Down
28 changes: 27 additions & 1 deletion app/src/form/components/QuestionField.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import {
TypeDate,
TypeImage,
Expand All @@ -8,14 +8,17 @@ import {
TypeText,
TypeNumber,
TypeGeo,
TypeCascade,
} from '../fields';
import { useField } from 'formik';
import { View, Text } from 'react-native';
import { styles } from '../styles';
import { FormState } from '../../store';
import { cascades } from '../../lib';

const QuestionField = ({ keyform, field: questionField, setFieldValue, values, validate }) => {
const [field, meta, helpers] = useField({ name: questionField.id, validate });
const [cascadeData, setCascadeData] = useState([]);

useEffect(() => {
if (meta.error && field.name) {
Expand Down Expand Up @@ -50,6 +53,18 @@ const QuestionField = ({ keyform, field: questionField, setFieldValue, values, v
setFieldValue(id, value);
};

const loadCascadeDataSource = async (source) => {
const { rows } = await cascades.loadDataSource(source);
setCascadeData(rows._array);
};

useEffect(() => {
if (questionField?.type === 'cascade' && questionField?.source?.file) {
const cascadeSource = questionField.source;
loadCascadeDataSource(cascadeSource);
}
}, []);

const renderField = () => {
switch (questionField?.type) {
case 'date':
Expand Down Expand Up @@ -115,6 +130,17 @@ const QuestionField = ({ keyform, field: questionField, setFieldValue, values, v
{...questionField}
/>
);
case 'cascade':
return (
<TypeCascade
keyform={keyform}
onChange={handleOnChangeField}
values={values}
{...questionField}
dataSource={cascadeData}
/>
);

default:
return (
<TypeInput
Expand Down
95 changes: 95 additions & 0 deletions app/src/form/fields/TypeCascade.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React, { useState, useEffect } from 'react';
import { View, Text } from 'react-native';
import { Dropdown } from 'react-native-element-dropdown';
import { FieldLabel } from '../support';
import { styles } from '../styles';

const TypeCascade = ({ onChange, values, keyform, id, name, source, dataSource = [] }) => {
const [dropdownItems, setDropdownItems] = useState([]);

const groupBy = (array, property) => {
const gd = array
.sort((a, b) => a?.name?.localeCompare(b?.name))
.reduce((groups, item) => {
const key = item[property];
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(item);
return groups;
}, {});
const groupedData = {};
Object.entries(gd).forEach(([key, value]) => {
groupedData[key] = value;
});
return groupedData;
};

const handleOnChange = (index, value) => {
const nextIndex = index + 1;
const updatedItems = dropdownItems
.slice(0, nextIndex)
.map((d, dx) => (dx === index ? { ...d, value } : d));

const options = dataSource?.filter((d) => d?.parent === value);

if (options.length) {
updatedItems.push({
options,
value: null,
});
}
const dropdownValues = updatedItems.filter((dd) => dd.value).map((dd) => dd.value);
const finalValues = updatedItems.length !== dropdownValues.length ? null : dropdownValues;
onChange(id, finalValues);

setDropdownItems(updatedItems);
};

useEffect(() => {
const parentID = source?.parent_id || 0;
const filterDs = dataSource.filter(
(ds) =>
ds?.parent === parentID ||
(values[id] && (values[id].includes(ds?.id) || values[id].includes(ds?.parent))),
);

if (dropdownItems.length === 0 && dataSource.length && filterDs.length) {
const groupedDs = groupBy(filterDs, 'parent');
const initialDropdowns = Object.values(groupedDs).map((options, ox) => {
return {
options,
value: values[id] ? values[id][ox] : null,
};
});
setDropdownItems(initialDropdowns);
}
}, [dataSource, dropdownItems, source, values, id]);

return (
<View testID="view-type-cascade">
<FieldLabel keyform={keyform} name={name} />
<Text testID="text-values" style={styles.cascadeValues}>
{values[id]}
</Text>
<View style={styles.cascadeContainer}>
{dropdownItems.map((item, index) => {
return (
<Dropdown
key={index}
labelField="name"
valueField="id"
testID={`dropdown-cascade-${index}`}
data={item?.options}
onChange={({ id: selectedID }) => handleOnChange(index, selectedID)}
value={item.value}
style={[styles.dropdownField]}
/>
);
})}
</View>
</View>
);
};

export default TypeCascade;
Loading

0 comments on commit 009cfeb

Please sign in to comment.