diff --git a/jest.config.js b/jest.config.js index e76b70d..7aae693 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,6 +12,7 @@ module.exports = { '^.+\\.ts?$': 'ts-jest', }, moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + modulePathIgnorePatterns: ['utils'], setupFilesAfterEnv: [ "@testing-library/jest-native/extend-expect", "./setup-tests.js" diff --git a/package.json b/package.json index 897e916..c2aafe1 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "react-native-vector-icons": "^8.1.0", "rn-fetch-blob-v2": "^0.14.1", "upper-case-first": "^2.0.2", - "wollok-ts": "^3.0.6" + "wollok-ts": "^3.0.8" }, "devDependencies": { "@babel/core": "^7.12.9", diff --git a/src/__tests__/App-test.tsx b/src/__tests__/App.spec.tsx similarity index 100% rename from src/__tests__/App-test.tsx rename to src/__tests__/App.spec.tsx diff --git a/src/__tests__/Problems.spec.tsx b/src/__tests__/Problems.spec.tsx new file mode 100644 index 0000000..749efb0 --- /dev/null +++ b/src/__tests__/Problems.spec.tsx @@ -0,0 +1,115 @@ +import { render, RenderAPI } from '@testing-library/react-native' +import React from 'react' +import { Node, Problem } from 'wollok-ts/dist/model' +import { ProblemIcon } from '../components/problems/ProblemIcon' +import { ProblemReporterButton } from '../components/problems/ProblemReporterButton' +import { ProblemModal } from '../components/problems/ProblemsModal' +import { methodFQN } from '../utils/wollok-helpers' +import ProjectProviderMock from './utils/ProjectProviderMock' +import { + clazz, + error, + field, + method, + problem, + sentence, + singleton, + test, + warning, + wReturn, +} from './utils/wollokProject' + +describe('ProblemIcon', () => { + it('is red on error', () => { + const rendered = render() + expectErrorIcon(rendered) + }) + + it('is yellow on warning', () => { + const rendered = render() + expectWarningIcon(rendered) + }) +}) + +describe('ProblemReporterButton', () => { + function renderProblemReporterButton(aNode: Node, ...problems: Problem[]) { + return render( + + + , + ) + } + + it('should not be shown if there is no related problems', () => { + const rendered = renderProblemReporterButton(singleton) + expect(rendered.toJSON()).toBeNull() + }) + + it('should show error if there is related warnings and problems', () => { + const rendered = renderProblemReporterButton( + singleton, + warning, + problem(singleton), + ) + expectErrorIcon(rendered) + }) + + describe('should render if there is any problem', () => { + function iconExistTest(name: string, node: Node, problem: Problem) { + it(name, () => { + const rendered = renderProblemReporterButton(node, problem) + expect(rendered.toJSON()).not.toBeNull() + }) + } + + iconExistTest('with problem node', singleton, warning) + + iconExistTest('inside method body', method, error) + + iconExistTest('inside test body', test, problem(test.sentences()[0])) + + iconExistTest('inside return expression', wReturn, problem(wReturn.value!)) + }) +}) + +describe('ProblemModal', () => { + function renderProblemModal(...problems: Problem[]) { + return render( + , + ) + } + + describe('should render description for', () => { + function problemDescriptionTest( + name: string, + description: string, + problem: Problem, + ) { + it(name, () => { + const { getByText } = renderProblemModal(problem) + expect(getByText(description)).toBeDefined() + }) + } + + problemDescriptionTest('singleton', singleton.name!, warning) + problemDescriptionTest('class', clazz.name!, problem(clazz)) + problemDescriptionTest('field', field.name, problem(field)) + problemDescriptionTest('method', methodFQN(method), problem(method)) + problemDescriptionTest('sentence', methodFQN(method), problem(sentence)) + }) +}) + +function expectErrorIcon({ UNSAFE_getByProps }: RenderAPI) { + expect( + UNSAFE_getByProps({ icon: 'alert-circle', color: 'red' }), + ).toBeDefined() +} + +function expectWarningIcon({ UNSAFE_getByProps }: RenderAPI) { + expect(UNSAFE_getByProps({ icon: 'alert', color: 'yellow' })).toBeDefined() +} diff --git a/src/__tests__/TestItem-test.tsx b/src/__tests__/TestItem.spec.tsx similarity index 92% rename from src/__tests__/TestItem-test.tsx rename to src/__tests__/TestItem.spec.tsx index f6df70d..0d2051e 100644 --- a/src/__tests__/TestItem-test.tsx +++ b/src/__tests__/TestItem.spec.tsx @@ -1,12 +1,12 @@ -import React from 'react' import { fireEvent, render } from '@testing-library/react-native' -import TestItem from '../components/tests/TestItem' -import { Body, Test } from 'wollok-ts/dist/model' -import { TestRun } from '../utils/wollok-helpers' +import React from 'react' import { ActivityIndicator } from 'react-native-paper' import { act, ReactTestInstance } from 'react-test-renderer' +import { Body, Test, WollokException } from 'wollok-ts' +import TestItem from '../components/tests/TestItem' import { theme } from '../theme' -import { WollokException } from 'wollok-ts/dist/interpreter/runtimeModel' +import { TestRun } from '../utils/wollok-helpers' +import ProjectProviderMock from './utils/ProjectProviderMock' const testMock = new Test({ name: 'TEST', @@ -48,12 +48,14 @@ const renderTest = (runner: () => TestRun = jest.fn()) => { UNSAFE_queryByProps, queryByText, } = render( - , + + + , ) return { runIcon: () => UNSAFE_getByProps({ icon: 'play-circle' }), diff --git a/src/components/ui/Body/sentences/tests/Variable.spec.tsx b/src/__tests__/Variable.spec.tsx similarity index 92% rename from src/components/ui/Body/sentences/tests/Variable.spec.tsx rename to src/__tests__/Variable.spec.tsx index 96148ce..5c85aaa 100644 --- a/src/components/ui/Body/sentences/tests/Variable.spec.tsx +++ b/src/__tests__/Variable.spec.tsx @@ -1,7 +1,7 @@ import React from 'react' import { Literal, Variable as VariableModel } from 'wollok-ts/dist/model' -import { renderWithTheme } from '../../../../../utils/test-helpers' -import { Variable } from '../Variable' +import { renderWithTheme } from './utils/test-helpers' +import { Variable } from '../components/Body/sentences/Variable' function renderVariable(variable: VariableModel) { const { UNSAFE_queryByProps, getByText } = renderWithTheme( diff --git a/src/components/ui/Body/sentences/tests/VisualSentence.spec.tsx b/src/__tests__/VisualSentence.spec.tsx similarity index 80% rename from src/components/ui/Body/sentences/tests/VisualSentence.spec.tsx rename to src/__tests__/VisualSentence.spec.tsx index 0eacd76..4313337 100644 --- a/src/components/ui/Body/sentences/tests/VisualSentence.spec.tsx +++ b/src/__tests__/VisualSentence.spec.tsx @@ -9,12 +9,12 @@ import { Sentence, Variable as VariableModel, } from 'wollok-ts/dist/model' -import { renderWithTheme } from '../../../../../utils/test-helpers' -import { Assignment } from '../Assignment' -import { Return } from '../Return' -import { Send } from '../Send' -import { Variable } from '../Variable' -import { VisualSentence } from '../VisualSentence' +import { Assignment } from '../components/Body/sentences/Assignment' +import { Return } from '../components/Body/sentences/Return' +import { Send } from '../components/Body/sentences/Send' +import { Variable } from '../components/Body/sentences/Variable' +import { VisualSentence } from '../components/Body/sentences/VisualSentence' +import { renderWithTheme } from './utils/test-helpers' describe('matching sentences with components', () => { it('should match a send sentence', () => { diff --git a/src/__tests__/utils/ProjectProviderMock.tsx b/src/__tests__/utils/ProjectProviderMock.tsx new file mode 100644 index 0000000..e2fd7e4 --- /dev/null +++ b/src/__tests__/utils/ProjectProviderMock.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import { Environment, Problem } from 'wollok-ts/dist/model' +import { ProjectContext } from '../../context/ProjectProvider' +import { ParentComponentProp } from '../../utils/type-helpers' + +export const initialContext = { + project: new Environment({ members: [] }), + name: 'Project Test', + changed: false, + problems: [] as Problem[], + actions: { + addEntity: jest.fn(), + addDescribe: jest.fn(), + addMember: jest.fn(), + changeMember: jest.fn(), + rebuildEnvironment: jest.fn(), + runTest: jest.fn(), + execution: jest.fn(), + save: jest.fn(), + }, +} + +type InitialProject = Partial + +const ProjectProviderMock = ({ + children, + ...props +}: ParentComponentProp) => { + return ( + + {children} + + ) +} + +export default ProjectProviderMock diff --git a/src/__tests__/utils/test-helpers.tsx b/src/__tests__/utils/test-helpers.tsx new file mode 100644 index 0000000..65ebec8 --- /dev/null +++ b/src/__tests__/utils/test-helpers.tsx @@ -0,0 +1,14 @@ +import { NavigationContainer } from '@react-navigation/native' +import { render } from '@testing-library/react-native' +import React from 'react' +import { theme } from '../../theme' +import ProjectProviderMock from './ProjectProviderMock' +import { OneOrMany } from '../../utils/type-helpers' + +export function renderWithTheme(children: OneOrMany) { + return render( + + {children} + , + ) +} diff --git a/src/__tests__/utils/wollokProject.ts b/src/__tests__/utils/wollokProject.ts new file mode 100644 index 0000000..59b8d04 --- /dev/null +++ b/src/__tests__/utils/wollokProject.ts @@ -0,0 +1,125 @@ +import { + Body, + Class, + Describe, + Environment, + Expression, + Field, + Import, + link, + Literal, + Method, + Node, + Package, + Parameter, + Problem, + Reference, + Return, + Send, + Singleton, + Test, + WRE, +} from 'wollok-ts' +import { fromJSON } from 'wollok-ts/dist/jsonUtils' + +const main = new Package({ + name: 'main', + members: [ + new Singleton({ + name: 'pepita', + members: [ + new Field({ + name: 'energia', + isConstant: false, + isProperty: true, + value: new Literal({ value: 100 }), + }), + new Method({ + name: 'come', + parameters: [ + new Parameter({ + name: 'comida', + }), + ], + body: new Body({ + sentences: [ + new Send({ + receiver: new Reference({ name: 'comida' }), + message: 'energiaQueAporta', + }), + ], + }), + }), + ], + }), + new Class({ + name: 'Entrenador', + }), + ], +}) + +const tests = new Package({ + name: 'tests', + imports: [ + new Import({ + entity: new Reference({ name: 'main' }), + isGeneric: true, + }), + ], + members: [ + new Describe({ + name: 'Main Describe', + members: [ + new Test({ + id: 'TEST', + name: 'test for testing', + body: new Body({ + sentences: [ + new Send({ + receiver: new Reference({ name: 'assert' }), + message: 'that', + args: [new Literal({ value: true })], + }), + ], + }), + }), + ], + }), + ], +}) + +export const project = link([main, tests], fromJSON(WRE)) + +export const singleton = project.getNodeByFQN('main.pepita') as Singleton + +export const clazz = project.getNodeByFQN('main.Entrenador') as Singleton + +export const describe = project.getNodeByFQN('tests.Main Describe') as Describe + +export const field = singleton.members[0] as Field + +export const method = singleton.members[1] as Method + +export const test = describe.members[0] as Test + +export const sentence = method.sentences()[0] + +export const wReturn = new Return({ value: sentence as Expression }) + +// PROBLEMS + +export const problem = (node: Node): Problem => ({ + node, + code: 'ERROR', + level: 'error', + values: [], +}) + +export const error = problem(sentence) + +export const warning: Problem = { + node: singleton, + code: 'SINGLETON_WARNING', + level: 'warning', + values: [], +} diff --git a/src/components/ui/Body/AssignmentFormModal.tsx b/src/components/Body/AssignmentFormModal.tsx similarity index 87% rename from src/components/ui/Body/AssignmentFormModal.tsx rename to src/components/Body/AssignmentFormModal.tsx index cf1f4df..d61176d 100644 --- a/src/components/ui/Body/AssignmentFormModal.tsx +++ b/src/components/Body/AssignmentFormModal.tsx @@ -10,10 +10,10 @@ import { Reference, Variable, } from 'wollok-ts/dist/model' -import { wTranslate } from '../../../utils/translation-helpers' -import { Referenciable } from '../../../utils/wollok-helpers' -import ExpressionInput from '../ExpressionInput' -import FormModal from '../FormModal/FormModal' +import { wTranslate } from '../../utils/translation-helpers' +import { Referenciable } from '../../utils/wollok-helpers' +import ExpressionInput from '../ui/ExpressionInput' +import FormModal from '../ui/FormModal/FormModal' type AssignmentFormModalProps = { variables: Referenciable[] @@ -62,7 +62,7 @@ export function AssignmentFormModal({ diff --git a/src/components/ui/Body/BodyMaker.tsx b/src/components/Body/BodyMaker.tsx similarity index 86% rename from src/components/ui/Body/BodyMaker.tsx rename to src/components/Body/BodyMaker.tsx index afcc972..5afb1ed 100644 --- a/src/components/ui/Body/BodyMaker.tsx +++ b/src/components/Body/BodyMaker.tsx @@ -2,19 +2,13 @@ import { useNavigation } from '@react-navigation/native' import React, { useState } from 'react' import { ScrollView, StyleSheet } from 'react-native' import { upperCaseFirst } from 'upper-case-first' -import { - Body, - Expression, - List, - Name, - Return, - Sentence, -} from 'wollok-ts/dist/model' -import { ExpressionOnSubmit } from '../../../pages/ExpressionMaker/ExpressionMaker' -import { wTranslate } from '../../../utils/translation-helpers' -import { Referenciable } from '../../../utils/wollok-helpers' -import MultiFabScreen from '../../FabScreens/MultiFabScreen' -import { SubmitCheckButton } from '../Header' +import { Body, Expression, Name, Return, Sentence } from 'wollok-ts/dist/model' +import { List } from 'wollok-ts/dist/extensions' +import { ExpressionOnSubmit } from '../../pages/ExpressionMaker' +import { wTranslate } from '../../utils/translation-helpers' +import { Referenciable } from '../../utils/wollok-helpers' +import MultiFabScreen from '../FabScreens/MultiFabScreen' +import { SubmitCheckButton } from '../ui/Header' import { AssignmentFormModal } from './AssignmentFormModal' import { returnIcon as returnIconName } from './sentences/Return' import { VisualSentence } from './sentences/VisualSentence' diff --git a/src/components/ui/Body/VariableForm.tsx b/src/components/Body/VariableForm.tsx similarity index 83% rename from src/components/ui/Body/VariableForm.tsx rename to src/components/Body/VariableForm.tsx index eb90d88..9d3520d 100644 --- a/src/components/ui/Body/VariableForm.tsx +++ b/src/components/Body/VariableForm.tsx @@ -2,11 +2,11 @@ import React, { useState } from 'react' import { Text, TextInput } from 'react-native-paper' import { upperCaseFirst } from 'upper-case-first' import { Expression, Name, Variable } from 'wollok-ts/dist/model' -import { wTranslate } from '../../../utils/translation-helpers' -import CheckIcon from '../CheckIcon' -import ExpressionInput from '../ExpressionInput' -import FormModal from '../FormModal/FormModal' -import { Row } from '../Row' +import { wTranslate } from '../../utils/translation-helpers' +import CheckIcon from '../ui/CheckIcon' +import ExpressionInput from '../ui/ExpressionInput' +import FormModal from '../ui/FormModal/FormModal' +import { Row } from '../ui/Row' type VariableFormModalProps = { onSubmit: (assignment: Variable) => void @@ -49,7 +49,7 @@ export function VariableFormModal({ diff --git a/src/components/ui/Body/sentences/Assignment.tsx b/src/components/Body/sentences/Assignment.tsx similarity index 58% rename from src/components/ui/Body/sentences/Assignment.tsx rename to src/components/Body/sentences/Assignment.tsx index 04459d5..7fc2ada 100644 --- a/src/components/ui/Body/sentences/Assignment.tsx +++ b/src/components/Body/sentences/Assignment.tsx @@ -1,13 +1,15 @@ import React from 'react' import { Assignment as AssignmentModel } from 'wollok-ts/dist/model' -import { ReferenceSegment } from '../../../expressions/expression-segment' -import { display } from '../../../expressions/ExpressionDisplay' -import { Row } from '../../Row' +import { ReferenceSegment } from '../../expressions/expression-segment' +import { display } from '../../expressions/ExpressionDisplay' +import { ProblemReporterButton } from '../../problems/ProblemReporterButton' +import { Row } from '../../ui/Row' import { ReferenceExpression } from './ReferenceExpression' export const Assignment = ({ assignment }: { assignment: AssignmentModel }) => { return ( + diff --git a/src/components/ui/Body/sentences/ReferenceExpression.tsx b/src/components/Body/sentences/ReferenceExpression.tsx similarity index 65% rename from src/components/ui/Body/sentences/ReferenceExpression.tsx rename to src/components/Body/sentences/ReferenceExpression.tsx index 14168fd..67d20a6 100644 --- a/src/components/ui/Body/sentences/ReferenceExpression.tsx +++ b/src/components/Body/sentences/ReferenceExpression.tsx @@ -1,8 +1,8 @@ import React from 'react' import { IconButton } from 'react-native-paper' -import { Expression } from 'wollok-ts' -import { ExpressionDisplay } from '../../../expressions/ExpressionDisplay' -import { Row } from '../../Row' +import { Expression } from 'wollok-ts/dist/model' +import { ExpressionDisplay } from '../../expressions/ExpressionDisplay' +import { Row } from '../../ui/Row' export const ReferenceExpression = (props: { expression: Expression }) => { return ( diff --git a/src/components/ui/Body/sentences/Return.tsx b/src/components/Body/sentences/Return.tsx similarity index 62% rename from src/components/ui/Body/sentences/Return.tsx rename to src/components/Body/sentences/Return.tsx index 573b79a..df5b6f3 100644 --- a/src/components/ui/Body/sentences/Return.tsx +++ b/src/components/Body/sentences/Return.tsx @@ -1,9 +1,10 @@ import React from 'react' import { IconButton } from 'react-native-paper' import { Return as ReturnModel } from 'wollok-ts/dist/model' -import { useTheme } from '../../../../theme' -import { ExpressionDisplay } from '../../../expressions/ExpressionDisplay' -import { Row } from '../../Row' +import { useTheme } from '../../../theme' +import { ExpressionDisplay } from '../../expressions/ExpressionDisplay' +import { ProblemReporterButton } from '../../problems/ProblemReporterButton' +import { Row } from '../../ui/Row' export const returnIcon = 'arrow-expand-up' @@ -11,6 +12,7 @@ export function Return(props: { returnSentence: ReturnModel }) { const theme = useTheme() return ( + { + return ( + + + + + ) +} diff --git a/src/components/ui/Body/sentences/Variable.tsx b/src/components/Body/sentences/Variable.tsx similarity index 64% rename from src/components/ui/Body/sentences/Variable.tsx rename to src/components/Body/sentences/Variable.tsx index 7b99bdd..094a747 100644 --- a/src/components/ui/Body/sentences/Variable.tsx +++ b/src/components/Body/sentences/Variable.tsx @@ -1,16 +1,18 @@ import React from 'react' import { IconButton, Text } from 'react-native-paper' import { Variable as VariableModel } from 'wollok-ts/dist/model' -import { useTheme } from '../../../../theme' -import { isNullExpression } from '../../../../utils/wollok-helpers' -import { ConstantVariableIcon } from '../../ConstantVariableIcon' -import { Row } from '../../Row' +import { useTheme } from '../../../theme' +import { isNullExpression } from '../../../utils/wollok-helpers' +import { ProblemReporterButton } from '../../problems/ProblemReporterButton' +import { ConstantVariableIcon } from '../../ui/ConstantVariableIcon' +import { Row } from '../../ui/Row' import { ReferenceExpression } from './ReferenceExpression' export const Variable = (props: { variable: VariableModel }) => { const theme = useTheme() return ( + {props.variable.name} diff --git a/src/components/ui/Body/sentences/VisualSentence.tsx b/src/components/Body/sentences/VisualSentence.tsx similarity index 88% rename from src/components/ui/Body/sentences/VisualSentence.tsx rename to src/components/Body/sentences/VisualSentence.tsx index aed8690..b88be21 100644 --- a/src/components/ui/Body/sentences/VisualSentence.tsx +++ b/src/components/Body/sentences/VisualSentence.tsx @@ -1,12 +1,13 @@ import React from 'react' import { Text } from 'react-native-paper' import { Sentence } from 'wollok-ts/dist/model' -import { wTranslate } from '../../../../utils/translation-helpers' -import { Row } from '../../Row' +import { wTranslate } from '../../../utils/translation-helpers' +import { Row } from '../../ui/Row' import { Assignment } from './Assignment' import { Return } from './Return' import { Send } from './Send' import { Variable } from './Variable' + export function VisualSentence({ sentence }: { sentence: Sentence }) { switch (sentence.kind) { case 'Send': diff --git a/src/components/entities/Entity/Entity.tsx b/src/components/entities/Entity/Entity.tsx index 01e9b87..52260a5 100644 --- a/src/components/entities/Entity/Entity.tsx +++ b/src/components/entities/Entity/Entity.tsx @@ -1,34 +1,31 @@ -import { useNavigation } from '@react-navigation/native' import React from 'react' import { List, withTheme } from 'react-native-paper' import { Module } from 'wollok-ts/dist/model' -import { HomeScreenNavigationProp } from '../../../pages/Home' +import { useNodeNavigation } from '../../../context/NodeNavigation' import { Theme } from '../../../theme' +import { ProblemReporterButton } from '../../problems/ProblemReporterButton' import { EntityKindIcon } from '../EntityKindIcon' import { stylesheet } from './styles' -type Props = { +type EntityComponentProps = { entity: Module theme: Theme } -function EntityComponent(props: Props) { - const styles = stylesheet(props.theme) - const navigation = useNavigation() - const goToEntityDetails = () => { - navigation.navigate('EntityStack', { - entityFQN: props.entity.fullyQualifiedName(), - }) - } +function EntityComponent({ entity, theme }: EntityComponentProps) { + const styles = stylesheet(theme) + const { goToNode } = useNodeNavigation() + const goToEntityDetails = () => goToNode(entity) return ( } + title={entity.name} + left={() => } + right={() => } /> ) } diff --git a/src/components/entity-detail/AccordionList.tsx b/src/components/entity-detail/AccordionList.tsx index eb86ad3..8fdceba 100644 --- a/src/components/entity-detail/AccordionList.tsx +++ b/src/components/entity-detail/AccordionList.tsx @@ -2,7 +2,7 @@ import { Theme, useTheme } from '@react-navigation/native' import React, { Key, useState } from 'react' import { StyleSheet, View } from 'react-native' import { Divider, List as ListComponent } from 'react-native-paper' -import { List } from 'wollok-ts/dist/model' +import { List } from 'wollok-ts/dist/extensions' type Props = { title: string diff --git a/src/components/entity-detail/AttributeItem/AttributeItem.tsx b/src/components/entity-detail/AttributeItem/AttributeItem.tsx index 9b8d6d6..8c85481 100644 --- a/src/components/entity-detail/AttributeItem/AttributeItem.tsx +++ b/src/components/entity-detail/AttributeItem/AttributeItem.tsx @@ -3,14 +3,14 @@ import { List, withTheme } from 'react-native-paper' import { Field } from 'wollok-ts/dist/model' import { Theme } from '../../../theme' import { ExpressionDisplay } from '../../expressions/ExpressionDisplay' +import { ProblemReporterButton } from '../../problems/ProblemReporterButton' import { ATTRIBUTE_ICONS } from '../attribute-icons' import styles from './styles' function AttributeItem(props: { attribute: Field; theme: Theme }) { - const { - attribute: { isProperty, isConstant, name, value }, - theme, - } = props + const { attribute, theme } = props + + const { isProperty, isConstant, name, value } = attribute const icons = [ { @@ -26,6 +26,7 @@ function AttributeItem(props: { attribute: Field; theme: Theme }) { return ( } description={() => value && } right={() => icons diff --git a/src/pages/EntityDetails/EntityDetails.tsx b/src/components/entity-detail/ModuleDetails.tsx similarity index 58% rename from src/pages/EntityDetails/EntityDetails.tsx rename to src/components/entity-detail/ModuleDetails.tsx index a7c1e1e..8ff4795 100644 --- a/src/pages/EntityDetails/EntityDetails.tsx +++ b/src/components/entity-detail/ModuleDetails.tsx @@ -3,21 +3,28 @@ import React, { useState } from 'react' import { ScrollView } from 'react-native-gesture-handler' import { List } from 'react-native-paper' import { upperCaseFirst } from 'upper-case-first' -import { Field, is, Method } from 'wollok-ts/dist/model' -import { AccordionList } from '../../components/entity-detail/AccordionList' -import AttributeItemComponent from '../../components/entity-detail/AttributeItem/AttributeItem' -import NewAttributeModal from '../../components/entity-detail/new-attribute-modal/NewAttributeModal' -import NewMethodModal from '../../components/entity-detail/new-method-modal/NewMethodModal' -import MultiFabScreen from '../../components/FabScreens/MultiFabScreen' -import { useEntity } from '../../context/EntityProvider' +import { Describe, Field, is, Method, Module } from 'wollok-ts/dist/model' +import { AccordionList } from './AccordionList' +import AttributeItemComponent from './AttributeItem/AttributeItem' +import NewAttributeModal from './new-attribute-modal/NewAttributeModal' +import NewMethodModal from './new-method-modal/NewMethodModal' +import MultiFabScreen from '../FabScreens/MultiFabScreen' +import { ProblemReporterButton } from '../problems/ProblemReporterButton' +import { useProject } from '../../context/ProjectProvider' import { wTranslate } from '../../utils/translation-helpers' import { methodFQN, methodLabel } from '../../utils/wollok-helpers' -import { EntityMemberScreenNavigationProp } from '../EntityMemberDetail' +import { EditorScreenNavigationProp } from '../../pages/Editor' -export const EntityDetails = function () { +export type ModuleDetailsProps = { + module: Exclude +} + +export const ModuleDetails = function ({ module }: ModuleDetailsProps) { const [methodModalVisible, setMethodModalVisible] = useState(false) const [attributeModalVisible, setAttributeModalVisible] = useState(false) - const { entity } = useEntity() + const { + actions: { addMember }, + } = useProject() return ( title={wTranslate('entityDetails.attributes').toUpperCase()} - items={entity.members.filter(is('Field')) as Field[]} + items={module.members.filter(is('Field')) as Field[]} VisualItem={AttributeItem} initialExpanded={true} /> title={wTranslate('entityDetails.methods').toUpperCase()} - items={entity.members.filter(is('Method')) as Method[]} + items={module.members.filter(is('Method')) as Method[]} VisualItem={MethodItem} initialExpanded={true} /> @@ -52,10 +59,13 @@ export const EntityDetails = function () { ) @@ -66,17 +76,18 @@ function AttributeItem({ item: attribute }: { item: Field }) { } function MethodItem({ item: method }: { item: Method }) { - const navigator = useNavigation() + const navigator = useNavigation() + function gotoMethod() { + navigator.navigate('Editor', { + fqn: methodFQN(method), + }) + } return ( - navigator.navigate('EntityMemberDetails', { - entityMember: method, - fqn: methodFQN(method), - }) - } + left={() => } + onPress={gotoMethod} /> ) } diff --git a/src/components/entity-detail/new-attribute-modal/NewAttributeModal.tsx b/src/components/entity-detail/new-attribute-modal/NewAttributeModal.tsx index 416c883..698fe28 100644 --- a/src/components/entity-detail/new-attribute-modal/NewAttributeModal.tsx +++ b/src/components/entity-detail/new-attribute-modal/NewAttributeModal.tsx @@ -1,34 +1,30 @@ import React, { useState } from 'react' import { StyleSheet, View } from 'react-native' -import { Text, TextInput, withTheme } from 'react-native-paper' +import { Text, TextInput } from 'react-native-paper' import { upperCaseFirst } from 'upper-case-first' import { Expression, Field } from 'wollok-ts/dist/model' -import { useEntity } from '../../../context/EntityProvider' -import { Theme } from '../../../theme' import { wTranslate } from '../../../utils/translation-helpers' +import { Visible } from '../../../utils/type-helpers' import CheckIcon from '../../ui/CheckIcon' import ExpressionInput from '../../ui/ExpressionInput' import FormModal from '../../ui/FormModal/FormModal' import { ATTRIBUTE_ICONS } from '../attribute-icons' -type Props = { - visible: boolean - setVisible: (visible: boolean) => void - theme: Theme +type AttributeFormModalProps = Visible & { + addNewField: (f: Field) => void + contextFQN: string } -const AttributeFormModal = (props: Props) => { - const { - actions: { addMember }, - entity, - } = useEntity() +const AttributeFormModal = ({ + visible, + setVisible, + addNewField, + contextFQN, +}: AttributeFormModalProps) => { const [name, setName] = useState('') const [isConstant, setConstant] = useState(false) const [isProperty, setProperty] = useState(false) const [initialValue, setInitialValue] = useState() - const { visible, setVisible } = props - - const styles = getStyles(props.theme) const checkboxes = [ { @@ -68,7 +64,7 @@ const AttributeFormModal = (props: Props) => { { } function newAttribute() { - addMember(new Field({ name, isConstant, isProperty, value: initialValue })) + addNewField( + new Field({ name, isConstant, isProperty, value: initialValue }), + ) } } -const getStyles = (_theme: Theme) => - StyleSheet.create({ - checkbox: { flexDirection: 'row', alignItems: 'center', marginVertical: 5 }, - constName: { fontSize: 16 }, - }) +const styles = StyleSheet.create({ + checkbox: { flexDirection: 'row', alignItems: 'center', marginVertical: 5 }, + constName: { fontSize: 16 }, +}) -export default withTheme(AttributeFormModal) +export default AttributeFormModal diff --git a/src/components/entity-detail/new-method-modal/NewMethodModal.tsx b/src/components/entity-detail/new-method-modal/NewMethodModal.tsx index febce88..b97cd7e 100644 --- a/src/components/entity-detail/new-method-modal/NewMethodModal.tsx +++ b/src/components/entity-detail/new-method-modal/NewMethodModal.tsx @@ -1,22 +1,20 @@ import React, { useState } from 'react' import { StyleSheet } from 'react-native' -import { Text, TextInput, withTheme } from 'react-native-paper' +import { Text, TextInput } from 'react-native-paper' import { upperCaseFirst } from 'upper-case-first' import { Body, Method, Parameter } from 'wollok-ts/dist/model' -import { useEntity } from '../../../context/EntityProvider' -import { Theme } from '../../../theme' import { wTranslate } from '../../../utils/translation-helpers' +import { Visible } from '../../../utils/type-helpers' import FormModal from '../../ui/FormModal/FormModal' import ParameterInput from './ParameterInput' -const NewMethodModal = (props: { - visible: boolean - setVisible: (value: boolean) => void - theme: Theme -}) => { - const { - actions: { addMember }, - } = useEntity() +type NewMethodModalProps = Visible & { addNewMethod: (m: Method) => void } + +const NewMethodModal = ({ + visible, + setVisible, + addNewMethod, +}: NewMethodModalProps) => { const [name, setName] = useState('') const [parameters, setParameters] = useState([]) const [nextParameter, setNextParameter] = useState('') @@ -26,8 +24,8 @@ const NewMethodModal = (props: { title={wTranslate('entityDetails.methodModal.newMethod')} resetForm={reset} onSubmit={newMethod} - setVisible={props.setVisible} - visible={props.visible}> + setVisible={setVisible} + visible={visible}> void +} + +export function ProblemIcon({ problem, onPress }: ProblemIconProps) { + return problem.level === 'error' ? ( + + ) : ( + + ) +} diff --git a/src/components/problems/ProblemReporterButton.tsx b/src/components/problems/ProblemReporterButton.tsx new file mode 100644 index 0000000..1bd1474 --- /dev/null +++ b/src/components/problems/ProblemReporterButton.tsx @@ -0,0 +1,52 @@ +import React, { useState } from 'react' +import { Node, Problem } from 'wollok-ts/dist/model' +import { useProject } from '../../context/ProjectProvider' +import { isError } from '../../utils/wollok-helpers' +import { ProblemIcon } from './ProblemIcon' +import { ProblemModal } from './ProblemsModal' + +interface ProblemReporterButtonProps { + node: Node + icon?: JSX.Element +} + +export function ProblemReporterButton({ + node, + icon, +}: ProblemReporterButtonProps) { + const [showProblems, setShowProblems] = useState(false) + const { problems } = useProject() + + const belongsTo = (problem: Problem): boolean => + node.match({ + Method: m => problem.node.ancestors().includes(m), + Test: t => problem.node.ancestors().includes(t), + Return: r => r.id === problem.node.id || r.value?.id === problem.node.id, + Node: n => n.id === problem.node.id, + }) + + const nodeProblems = problems.filter(belongsTo) + + if (!nodeProblems.length) { + return null + } + + const maybeError = nodeProblems.find(isError) + + return ( + <> + {icon || ( + setShowProblems(true)} + /> + )} + + + + ) +} diff --git a/src/components/problems/ProblemsModal.tsx b/src/components/problems/ProblemsModal.tsx new file mode 100644 index 0000000..58a3328 --- /dev/null +++ b/src/components/problems/ProblemsModal.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import { ScrollView } from 'react-native' +import { List } from 'react-native-paper' +import { Node, Problem } from 'wollok-ts/dist/model' +import { wTranslate } from '../../utils/translation-helpers' +import { methodFQN } from '../../utils/wollok-helpers' +import FormModal, { FormModalProps } from '../ui/FormModal/FormModal' +import { ProblemIcon } from './ProblemIcon' + +interface ProblemsModalProp { + problems: Problem[] + onSelect?: (p: Problem) => void +} + +export function ProblemModal({ + problems, + onSelect, + visible, + setVisible, +}: ProblemsModalProp & Pick) { + const nodeDescription = (n: Node): string | undefined => + n.match({ + Entity: s => s.name, + Field: f => f.name, + Method: methodFQN, + Test: t => t.name, + Body: b => nodeDescription(b.parent), + Sentence: a => nodeDescription(a.parent), + }) + + return ( + setVisible(false)}> + + {problems.map((problem, i) => ( + onSelect && onSelect(problem)} + title={wTranslate(`problem.${problem.code}`)} + titleNumberOfLines={2} + left={() => } + description={nodeDescription(problem.node)} + /> + ))} + + + ) +} diff --git a/src/components/select-project/NewProjectModal.tsx b/src/components/projects/NewProjectModal.tsx similarity index 100% rename from src/components/select-project/NewProjectModal.tsx rename to src/components/projects/NewProjectModal.tsx diff --git a/src/components/projects/ProjectHeader.tsx b/src/components/projects/ProjectHeader.tsx new file mode 100644 index 0000000..8f7af33 --- /dev/null +++ b/src/components/projects/ProjectHeader.tsx @@ -0,0 +1,52 @@ +import React, { useState } from 'react' +import { Badge, IconButton } from 'react-native-paper' +import { Node } from 'wollok-ts/dist/model' +import { useNodeNavigation } from '../../context/NodeNavigation' +import { useProject } from '../../context/ProjectProvider' +import { ProblemModal } from '../problems/ProblemsModal' +import { Row } from '../ui/Row' + +interface ProjectHeaderProp { + pushMessage: (tag: string) => void +} + +export function ProjectHeader({ pushMessage }: ProjectHeaderProp) { + const [showProblems, setShowProblems] = useState(false) + const { + changed, + problems, + actions: { save }, + } = useProject() + + const { goToNode } = useNodeNavigation() + + const goto = (n: Node): void => { + goToNode(n) + setShowProblems(false) + } + + return ( + + { + setShowProblems(true) + }} + /> + {problems.length} + save().then(() => pushMessage('saved'))} + /> + + goto(p.node)} + /> + + ) +} diff --git a/src/components/tests/DescribeItem.tsx b/src/components/tests/DescribeItem.tsx index ba4133b..d34e1ab 100644 --- a/src/components/tests/DescribeItem.tsx +++ b/src/components/tests/DescribeItem.tsx @@ -1,7 +1,7 @@ -import { useNavigation } from '@react-navigation/native' import React from 'react' import { List, withTheme } from 'react-native-paper' import { Describe } from 'wollok-ts/dist/model' +import { useNodeNavigation } from '../../context/NodeNavigation' import { Theme } from '../../theme' import { stylesheet } from '../entities/Entity/styles' import IconImage from '../ui/IconImage' @@ -15,12 +15,8 @@ type Props = { // TODO: Merge with Entity component function DescribeItem({ describe, theme }: Props) { const styles = stylesheet(theme) - const navigation = useNavigation() - const goToEntityDetails = () => { - navigation.navigate('EntityStack', { - entityFQN: describe.fullyQualifiedName(), - }) - } + const { goToNode } = useNodeNavigation() + const goToEntityDetails = () => goToNode(describe) return ( void -}) { - const { - actions: { addMember }, - } = useEntity() +type NewTestModalProps = Pick & { + addNewTest: (t: Test) => void +} + +function NewTestModal({ visible, setVisible, addNewTest }: NewTestModalProps) { const [name, setName] = useState(initialName) return ( + setVisible={setVisible} + visible={visible}> } + left={() => ( + <> + + + + )} right={() => ( <> {testRun?.exception?.message && ( diff --git a/src/components/tests/Tests.tsx b/src/components/tests/Tests.tsx index 51494ef..71568af 100644 --- a/src/components/tests/Tests.tsx +++ b/src/components/tests/Tests.tsx @@ -2,27 +2,28 @@ import { useNavigation } from '@react-navigation/native' import React, { useState } from 'react' import { ScrollView } from 'react-native-gesture-handler' import { upperCaseFirst } from 'upper-case-first' -import { is, Test } from 'wollok-ts/dist/model' -import { useEntity } from '../../context/EntityProvider' +import { Describe, is, Test } from 'wollok-ts/dist/model' import { useProject } from '../../context/ProjectProvider' -import { EntityMemberScreenNavigationProp } from '../../pages/EntityMemberDetail' +import { EditorScreenNavigationProp } from '../../pages/Editor' import { wTranslate } from '../../utils/translation-helpers' import MultiFabScreen from '../FabScreens/MultiFabScreen' import NewTestModal from './NewTestModal' import TestItem from './TestItem' -export const Tests = function () { +export type TestsProps = { + describe: Describe +} + +export const Tests = function ({ describe }: TestsProps) { const [testNewModalVisible, setTestNewModalVisible] = useState(false) const { - actions: { runTest }, + actions: { runTest, addMember }, } = useProject() - const { entity } = useEntity() - const tests = entity.members.filter(is('Test')) as Test[] + const tests = describe.members.filter(is('Test')) as Test[] - const navigator = useNavigation() + const navigator = useNavigation() function navigateTo(test: Test) { - navigator.navigate('EntityMemberDetails', { - entityMember: test, + navigator.navigate('Editor', { fqn: test.fullyQualifiedName(), }) } @@ -63,6 +64,7 @@ export const Tests = function () { ) diff --git a/src/components/ui/Body/sentences/Send.tsx b/src/components/ui/Body/sentences/Send.tsx deleted file mode 100644 index 1e709b5..0000000 --- a/src/components/ui/Body/sentences/Send.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react' -import { Send as SendModel } from 'wollok-ts/dist/model' -import { ExpressionDisplay } from '../../../expressions/ExpressionDisplay' - -export const Send = ({ send }: { send: SendModel }) => { - return -} diff --git a/src/components/ui/ConstantVariableIcon.tsx b/src/components/ui/ConstantVariableIcon.tsx index b5e096e..26824de 100644 --- a/src/components/ui/ConstantVariableIcon.tsx +++ b/src/components/ui/ConstantVariableIcon.tsx @@ -1,6 +1,6 @@ import React from 'react' import { IconButton } from 'react-native-paper' -import { Variable } from 'wollok-ts' +import { Variable } from 'wollok-ts/dist/model' import { useTheme } from '../../theme' export const ConstantVariableIcon = (props: { variable: Variable }) => { diff --git a/src/components/ui/ExpressionInput.tsx b/src/components/ui/ExpressionInput.tsx index b665c1e..99eba36 100644 --- a/src/components/ui/ExpressionInput.tsx +++ b/src/components/ui/ExpressionInput.tsx @@ -3,7 +3,7 @@ import React from 'react' import { StyleSheet, View } from 'react-native' import { IconButton, Text, withTheme } from 'react-native-paper' import { Expression, Name } from 'wollok-ts/dist/model' -import { ExpressionMakerScreenProp } from '../../pages/ExpressionMaker/ExpressionMaker' +import { ExpressionMakerScreenProp } from '../../pages/ExpressionMaker' import { Theme } from '../../theme' import { ExpressionDisplay } from '../expressions/ExpressionDisplay' @@ -11,7 +11,7 @@ type Props = { value?: Expression setValue: (expression?: Expression) => void theme: Theme - fqn: Name + contextFQN: Name inputPlaceholder: string } @@ -21,7 +21,7 @@ const ExpressionInput = (props: Props) => { const goToExpressionMaker = () => { navigation.push('ExpressionMaker', { onSubmit: setValue, - contextFQN: props.fqn, + contextFQN: props.contextFQN, initialExpression: value, }) } diff --git a/src/components/ui/FormModal/FormModal.tsx b/src/components/ui/FormModal/FormModal.tsx index 9775eb7..e926404 100644 --- a/src/components/ui/FormModal/FormModal.tsx +++ b/src/components/ui/FormModal/FormModal.tsx @@ -9,20 +9,20 @@ import { } from 'react-native-paper' import { Theme } from '../../../theme' import { wTranslate } from '../../../utils/translation-helpers' -import { ParentComponentProp } from '../../../utils/type-helpers' +import { ParentComponentProp, Visible } from '../../../utils/type-helpers' import { stylesheet } from './styles' -function FormModal( - props: ParentComponentProp<{ - visible: boolean - setVisible: (value: boolean) => void +export type FormModalProps = ParentComponentProp< + Visible & { onSubmit: () => void resetForm?: () => void title?: string valid?: boolean theme: Theme - }>, -) { + } +> + +function FormModal(props: FormModalProps) { const styles = stylesheet(props.theme) const disabledSubmit = props.valid === undefined ? false : !props.valid diff --git a/src/context/EntityProvider.tsx b/src/context/EntityProvider.tsx deleted file mode 100644 index 0596244..0000000 --- a/src/context/EntityProvider.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React, { createContext } from 'react' -import { Module } from 'wollok-ts/dist/model' -import { ParentComponentProp } from '../utils/type-helpers' -import { EntityMember } from '../utils/wollok-helpers' -import { createContextHook } from './create-context-hook' -import { useProject } from './ProjectProvider' - -export const EntityContext = createContext<{ - entity: Module - actions: Actions -} | null>(null) - -type Actions = { - addMember: (newMember: EntityMember) => void - changeMember: (oldMember: EntityMember, newMember: EntityMember) => void -} - -export function EntityProvider( - props: ParentComponentProp<{ - entity: Module - }>, -) { - const { children, entity } = props - const { - actions: { rebuildEnvironment }, - } = useProject() - - const addMember = (newMember: EntityMember) => { - rebuildEnvironment( - entity.copy({ - members: [...entity.members, newMember], - }) as Module, - ) - } - - const changeMember = (oldMember: EntityMember, newMember: EntityMember) => { - rebuildEnvironment( - entity.copy({ - members: [...entity.members.filter(m => m !== oldMember), newMember], - }) as Module, - ) - } - - const initialContext = { - entity: entity, - actions: { addMember, changeMember }, - } - return ( - - {children} - - ) -} - -export const useEntity = createContextHook(EntityContext, { - hookName: 'useEntity', - contextName: 'EntityProvider', -}) diff --git a/src/context/ExpressionContextProvider.tsx b/src/context/ExpressionContextProvider.tsx index 19a1d64..fff5446 100644 --- a/src/context/ExpressionContextProvider.tsx +++ b/src/context/ExpressionContextProvider.tsx @@ -1,5 +1,6 @@ import React, { createContext, useState } from 'react' -import { List, Method, Module, Name, Test } from 'wollok-ts/dist/model' +import { List } from 'wollok-ts/dist/extensions' +import { Method, Module, Name, Test } from 'wollok-ts/dist/model' import { ParentComponentProp } from '../utils/type-helpers' import { Named } from '../utils/wollok-helpers' import { createContextHook } from './create-context-hook' diff --git a/src/context/NodeNavigation.tsx b/src/context/NodeNavigation.tsx new file mode 100644 index 0000000..452a496 --- /dev/null +++ b/src/context/NodeNavigation.tsx @@ -0,0 +1,52 @@ +import { useNavigation } from '@react-navigation/native' +import React, { createContext } from 'react' +import { Entity, Method, Module, Node, Test } from 'wollok-ts/dist/model' +import { HomeScreenNavigationProp } from '../pages/Home' +import { ParentComponentProp } from '../utils/type-helpers' +import { entityMemberFQN, EntityMemberWithBody } from '../utils/wollok-helpers' +import { createContextHook } from './create-context-hook' + +export type Context = Module | Method | Test + +export const ContextContext = createContext<{ + goToNode: (n: Node) => void +} | null>(null) + +export function NodeNavigationProvider({ children }: ParentComponentProp) { + const navigation = useNavigation() + + const goToEntityDetails = (entity: Entity) => { + navigation.navigate('EntityDetails', { + entityFQN: entity.fullyQualifiedName(), + }) + } + const goToEditor = (entityMember: EntityMemberWithBody) => { + navigation.navigate('Editor', { + fqn: entityMemberFQN(entityMember), + }) + } + + const goToNode = (n: Node): void => { + n.match({ + Method: goToEditor, + Test: goToEditor, + Describe: goToEntityDetails, + Module: goToEntityDetails, + Field: f => goToEntityDetails(f.parent), + Body: b => goToNode(b.parent), + Sentence: e => goToNode(e.parent), + Expression: e => goToNode(e.parent), + }) + } + + const init = { goToNode } + + return ( + {children} + ) +} + +export const useNodeNavigation = createContextHook(ContextContext, { + contextName: 'NodeNavigationProvider', + hookName: 'useNodeNavigation', +}) diff --git a/src/context/ProjectProvider.tsx b/src/context/ProjectProvider.tsx index 80592ff..164a3ba 100644 --- a/src/context/ProjectProvider.tsx +++ b/src/context/ProjectProvider.tsx @@ -10,27 +10,43 @@ import { Module, Name, Package, + Problem, Reference, Test, } from 'wollok-ts/dist/model' +import validate from 'wollok-ts/dist/validator' +import { saveProject } from '../services/persistance.service' import { ParentComponentProp } from '../utils/type-helpers' -import { executionFor, interpretTest, TestRun } from '../utils/wollok-helpers' +import { + EntityMember, + executionFor, + interpretTest, + TestRun, +} from '../utils/wollok-helpers' import { createContextHook } from './create-context-hook' export const mainPackageName = 'main' +export const testsPackageName = 'tests' export const ProjectContext = createContext<{ project: Environment name: string + changed: boolean + problems: Problem[] actions: Actions } | null>(null) type Actions = { + rebuildEnvironment: (entity: Entity) => void addEntity: (module: Module) => void addDescribe: (test: Describe) => void - rebuildEnvironment: (entity: Entity) => void + addMember: (parent: Module) => (newMember: EntityMember) => void + changeMember: ( + parent: Module, + ) => (oldMember: EntityMember, newMember: EntityMember) => void runTest: (test: Test) => TestRun execution: (test: Test) => ExecutionDirector + save: () => Promise } export function ProjectProvider( @@ -42,6 +58,12 @@ export function ProjectProvider( const [project, setProject] = useState( link(props.initialProject.members), ) + const [changed, setChanged] = useState(false) + const [problems, setProblems] = useState( + validateProject(project) as Problem[], + ) + + /////////////////////////////////// BUILD ////////////////////////////////// function buildEnvironment( name: Name, @@ -61,6 +83,31 @@ export function ProjectProvider( return link([pack], base ?? project) } + function rebuildEnvironment(entity: Entity) { + const packageName = entity.is('Describe') + ? testsPackageName + : mainPackageName + const newProject = buildEnvironment(packageName, [entity]) + setProject(newProject) + setChanged(true) + setProblems(validateProject(newProject) as Problem[]) + } + + function validateProject(_project: Environment) { + const targetPackages = [ + _project.getNodeByFQN(mainPackageName), + _project.getNodeByFQN(testsPackageName), + ] + const belongsToTargetProject = (p: Problem) => + targetPackages.some(target => p.node.ancestors().includes(target)) + + return validate(_project).filter(belongsToTargetProject) + } + + /////////////////////////////////// BUILD ////////////////////////////////// + + /////////////////////////////////// ENTITIES ////////////////////////////////// + function addEntity(newEntity: Module) { rebuildEnvironment(newEntity) } @@ -69,12 +116,27 @@ export function ProjectProvider( rebuildEnvironment(newDescribe) } - function rebuildEnvironment(entity: Entity) { - const packageName = entity.is('Describe') ? 'tests' : mainPackageName - setProject(buildEnvironment(packageName, [entity])) - //TODO: Run validations + const addMember = (entity: Module) => (newMember: EntityMember) => { + rebuildEnvironment( + entity.copy({ + members: [...entity.members, newMember], + }) as Module, + ) } + const changeMember = + (entity: Module) => (oldMember: EntityMember, newMember: EntityMember) => { + rebuildEnvironment( + entity.copy({ + members: [...entity.members.filter(m => m !== oldMember), newMember], + }) as Module, + ) + } + + /////////////////////////////////// ENTITIES ////////////////////////////////// + + /////////////////////////////////// EXECUTION ////////////////////////////////// + function runTest(test: Test) { return interpretTest(test, project) } @@ -83,10 +145,28 @@ export function ProjectProvider( return executionFor(test, project) } + /////////////////////////////////// EXECUTION ////////////////////////////////// + + async function save() { + await saveProject(props.projectName, project) + setChanged(false) + } + const initialContext = { project, name: props.projectName, - actions: { addEntity, addDescribe, rebuildEnvironment, runTest, execution }, + changed, + problems, + actions: { + addEntity, + addDescribe, + addMember, + changeMember, + rebuildEnvironment, + runTest, + execution, + save, + }, } return ( diff --git a/src/context/initialProject.ts b/src/context/initialProject.ts index 6385e6a..b2512e1 100644 --- a/src/context/initialProject.ts +++ b/src/context/initialProject.ts @@ -5,7 +5,6 @@ import { Describe, Environment, Field, - fromJSON, Import, Literal, Method, @@ -16,6 +15,7 @@ import { Singleton, Test, } from 'wollok-ts/dist/model' +import { fromJSON } from 'wollok-ts/dist/jsonUtils' import WRE from 'wollok-ts/dist/wre/wre.json' import { mainPackageName } from './ProjectProvider' diff --git a/src/pages/NewMessageCall.tsx b/src/pages/ArgumentsMaker.tsx similarity index 86% rename from src/pages/NewMessageCall.tsx rename to src/pages/ArgumentsMaker.tsx index dd7b9f2..93137fd 100644 --- a/src/pages/NewMessageCall.tsx +++ b/src/pages/ArgumentsMaker.tsx @@ -7,14 +7,15 @@ import { Expression, Send } from 'wollok-ts/dist/model' import ExpressionInput from '../components/ui/ExpressionInput' import { SubmitCheckButton } from '../components/ui/Header' import { wTranslate } from '../utils/translation-helpers' -import { EntityStackParamList } from './EntityStack' +import { methodLabel } from '../utils/wollok-helpers' +import { ProjectStackParamList } from './ProjectNavigator' -export function NewMessageCall({ +export function ArgumentsMaker({ route: { params: { method, receiver, contextFQN, onSubmit }, }, }: { - route: RouteProp + route: RouteProp }) { const [args, setArguments] = useState<(Expression | undefined)[]>( method.parameters.map(() => undefined), @@ -24,6 +25,7 @@ export function NewMessageCall({ React.useLayoutEffect(() => { navigation.setOptions({ + title: methodLabel(method), //TODO: Show receiver? headerRight: () => ( a === undefined)} @@ -53,7 +55,7 @@ export function NewMessageCall({ {_.name} setParameter(i, expression)} value={args[i]} inputPlaceholder={upperCaseFirst( diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx new file mode 100644 index 0000000..43ee6eb --- /dev/null +++ b/src/pages/Editor.tsx @@ -0,0 +1,56 @@ +import { RouteProp, useNavigation } from '@react-navigation/native' +import { StackNavigationProp } from '@react-navigation/stack' +import React from 'react' +import { Body } from 'wollok-ts/dist/model' +import { BodyMaker } from '../components/Body/BodyMaker' +import { useProject } from '../context/ProjectProvider' +import { + allScopedVariables, + entityMemberByFQN, + EntityMemberWithBody, +} from '../utils/wollok-helpers' +import { ProjectStackParamList } from './ProjectNavigator' + +export type EditorScreenNavigationProp = StackNavigationProp< + ProjectStackParamList, + 'Editor' +> + +type Route = RouteProp + +export const Editor = ({ + route: { + params: { fqn }, + }, +}: { + route: Route +}) => { + const { + project, + actions: { changeMember }, + } = useProject() + const entity = entityMemberByFQN(project, fqn) + const parent = entity.parent + + const navigation = useNavigation() + React.useLayoutEffect(() => { + navigation.setOptions({ + title: entity.name, + headerTitleAlign: 'center', + animationEnabled: false, + }) + }, [navigation, entity]) + + function setBody(body: Body) { + changeMember(parent)(entity, entity.copy({ body }) as EntityMemberWithBody) + } + + return ( + + ) +} diff --git a/src/pages/EntityDetails.tsx b/src/pages/EntityDetails.tsx new file mode 100644 index 0000000..ef0fb15 --- /dev/null +++ b/src/pages/EntityDetails.tsx @@ -0,0 +1,33 @@ +import { RouteProp, useNavigation } from '@react-navigation/native' +import React from 'react' +import { Module } from 'wollok-ts/dist/model' +import { ModuleDetails } from '../components/entity-detail/ModuleDetails' +import { Tests } from '../components/tests/Tests' +import { useProject } from '../context/ProjectProvider' +import { ProjectStackParamList } from './ProjectNavigator' + +export type EntityDetailsRoute = RouteProp< + ProjectStackParamList, + 'EntityDetails' +> + +function EntityDetails(props: { route: EntityDetailsRoute }) { + const { project } = useProject() + const entity = project.getNodeByFQN(props.route.params.entityFQN) + + const navigation = useNavigation() + React.useLayoutEffect(() => { + navigation.setOptions({ + title: entity.name, + headerTitleAlign: 'center', + }) + }, [navigation, entity]) + + return entity.is('Describe') ? ( + + ) : ( + + ) +} + +export default EntityDetails diff --git a/src/pages/EntityMemberDetail.tsx b/src/pages/EntityMemberDetail.tsx deleted file mode 100644 index afb6a92..0000000 --- a/src/pages/EntityMemberDetail.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { RouteProp } from '@react-navigation/native' -import { StackNavigationProp } from '@react-navigation/stack' -import React from 'react' -import { Body } from 'wollok-ts/dist/model' -import { BodyMaker } from '../components/ui/Body/BodyMaker' -import { useEntity } from '../context/EntityProvider' -import { - allScopedVariables, - EntityMemberWithBody, -} from '../utils/wollok-helpers' -import { EntityStackParamList } from './EntityStack' - -export type EntityMemberScreenNavigationProp = StackNavigationProp< - EntityStackParamList, - 'EntityMemberDetails' -> - -type Route = RouteProp - -export const EntityMemberDetail = ({ - route: { - params: { entityMember, fqn }, - }, -}: { - route: Route -}) => { - const { - actions: { changeMember }, - } = useEntity() - - function setBody(body: Body) { - changeMember( - entityMember, - entityMember.copy({ body }) as EntityMemberWithBody, - ) - } - - return ( - - ) -} diff --git a/src/pages/EntityStack.tsx b/src/pages/EntityStack.tsx deleted file mode 100644 index 5799353..0000000 --- a/src/pages/EntityStack.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { RouteProp } from '@react-navigation/native' -import { createStackNavigator } from '@react-navigation/stack' -import React from 'react' -import { Expression, Method, Module, Name, Send } from 'wollok-ts/dist/model' -import { Tests } from '../components/tests/Tests' -import { EntityProvider } from '../context/EntityProvider' -import { useProject } from '../context/ProjectProvider' -import { wTranslate } from '../utils/translation-helpers' -import { - entityMemberLabel, - EntityMemberWithBody, -} from '../utils/wollok-helpers' -import { EntityDetails } from './EntityDetails/EntityDetails' -import { EntityMemberDetail } from './EntityMemberDetail' -import ExpressionMaker, { - ExpressionOnSubmit, -} from './ExpressionMaker/ExpressionMaker' -import { NewMessageCall } from './NewMessageCall' -import { ProjectStackParamList } from './ProjectNavigator' - -export type EntityStackRoute = RouteProp - -export type EntityStackParamList = { - EntityDetails: undefined - Tests: undefined - EntityMemberDetails: { - entityMember: EntityMemberWithBody - fqn: Name - } - ExpressionMaker: { - onSubmit: ExpressionOnSubmit - contextFQN: Name - initialExpression?: Expression - } - NewMessageSend: { - receiver: Expression - method: Method - contextFQN: Name - onSubmit: (s: Send) => void - } -} - -export default function (props: { route: EntityStackRoute }) { - const { project } = useProject() - const Stack = createStackNavigator() - const entity = project.getNodeByFQN(props.route.params.entityFQN) - return ( - - - {entity.is('Describe') ? ( - - ) : ( - - )} - ({ - title: entityMemberLabel(methodRoute.params.entityMember), - })} - /> - - ({ title: route.route.params.method.name })} - /> - - - ) -} diff --git a/src/pages/ExpressionMaker/ExpressionMaker.tsx b/src/pages/ExpressionMaker.tsx similarity index 72% rename from src/pages/ExpressionMaker/ExpressionMaker.tsx rename to src/pages/ExpressionMaker.tsx index 33b120b..66e9e6c 100644 --- a/src/pages/ExpressionMaker/ExpressionMaker.tsx +++ b/src/pages/ExpressionMaker.tsx @@ -4,30 +4,25 @@ import React, { useState } from 'react' import { StyleSheet, View } from 'react-native' import { ScrollView } from 'react-native-gesture-handler' import { Button, List, TextInput } from 'react-native-paper' -import { Expression, Module } from 'wollok-ts/dist/model' -import { ListLiterals } from '../../components/expressions/expression-lists/literals-list' -import { ListMessages } from '../../components/expressions/expression-lists/messages-list' -import { ListSingletons } from '../../components/expressions/expression-lists/singletons-list' -import { ListVariables } from '../../components/expressions/expression-lists/variables-list' -import { ExpressionDisplay } from '../../components/expressions/ExpressionDisplay' -import { SubmitCheckButton } from '../../components/ui/Header' +import { Expression } from 'wollok-ts/dist/model' +import { ListLiterals } from '../components/expressions/expression-lists/literals-list' +import { ListMessages } from '../components/expressions/expression-lists/messages-list' +import { ListSingletons } from '../components/expressions/expression-lists/singletons-list' +import { ListVariables } from '../components/expressions/expression-lists/variables-list' +import { ExpressionDisplay } from '../components/expressions/ExpressionDisplay' +import { SubmitCheckButton } from '../components/ui/Header' import { Context, ExpressionContextProvider, useExpressionContext, -} from '../../context/ExpressionContextProvider' -import { useProject } from '../../context/ProjectProvider' -import { wTranslate } from '../../utils/translation-helpers' -import { isMethodFQN, methodByFQN } from '../../utils/wollok-helpers' -import { EntityStackParamList } from '../EntityStack' - -export type ExpressionMakerProp = RouteProp< - EntityStackParamList, - 'ExpressionMaker' -> +} from '../context/ExpressionContextProvider' +import { useProject } from '../context/ProjectProvider' +import { wTranslate } from '../utils/translation-helpers' +import { entityMemberByFQN } from '../utils/wollok-helpers' +import { ProjectStackParamList } from './ProjectNavigator' export type ExpressionMakerScreenProp = StackNavigationProp< - EntityStackParamList, + ProjectStackParamList, 'ExpressionMaker' > @@ -55,6 +50,9 @@ function ExpressionMaker(props: { const navigation = useNavigation() React.useLayoutEffect(() => { navigation.setOptions({ + title: wTranslate('expression.title'), + headerTitleAlign: 'center', + animationEnabled: false, headerRight: () => ( + const styles = StyleSheet.create({ view: { display: 'flex', maxHeight: '85%' }, }) @@ -124,12 +127,11 @@ export default function ({ params: { contextFQN, onSubmit, initialExpression }, }, }: { - route: RouteProp + route: ExpressionMakerRouteProp }) { const { project } = useProject() - const context: Context = isMethodFQN(contextFQN) - ? methodByFQN(project, contextFQN) - : project.getNodeByFQN(contextFQN) + const context: Context = entityMemberByFQN(project, contextFQN) + return ( { navigation.setOptions({ title: name, headerTitleAlign: 'center', - headerRight: () => ( - saveProject(name, project).then(() => setSaved(true))} - /> - ), + headerRight: () => , }) }, [navigation, project, name]) @@ -42,7 +43,7 @@ export function Home() { }} /> { - setSaved(false) + setShowMessage(false) }} duration={2000} wrapperStyle={{ marginBottom: '20%' }}> - {wTranslate('project.saved')} + {wTranslate(`project.${message}`)} ) diff --git a/src/pages/ProjectNavigator.tsx b/src/pages/ProjectNavigator.tsx index 25d60ee..c46e715 100644 --- a/src/pages/ProjectNavigator.tsx +++ b/src/pages/ProjectNavigator.tsx @@ -1,15 +1,36 @@ import { RouteProp } from '@react-navigation/core' -import { createStackNavigator } from '@react-navigation/stack' +import { + createStackNavigator, + StackNavigationProp, +} from '@react-navigation/stack' import React from 'react' -import { Name } from 'wollok-ts/dist/model' +import { Expression, Method, Name, Send } from 'wollok-ts/dist/model' import { RootStackParamList } from '../App' +import { NodeNavigationProvider } from '../context/NodeNavigation' import { ProjectProvider } from '../context/ProjectProvider' -import EntityStack from './EntityStack' +import { ArgumentsMaker } from './ArgumentsMaker' +import { Editor } from './Editor' +import EntityDetails from './EntityDetails' +import ExpressionMaker, { ExpressionOnSubmit } from './ExpressionMaker' import { Home } from './Home' export type ProjectStackParamList = { Home: undefined - EntityStack: { entityFQN: Name } + EntityDetails: { entityFQN: Name } + Editor: { + fqn: Name + } + ExpressionMaker: { + onSubmit: ExpressionOnSubmit + contextFQN: Name + initialExpression?: Expression + } + ArgumentsMaker: { + receiver: Expression + method: Method + contextFQN: Name + onSubmit: (s: Send) => void + } } const Stack = createStackNavigator() @@ -19,20 +40,26 @@ export type ProjectStackRoute = RouteProp< 'ProjectNavigator' > +export type ProjectScreenNavigationProp = StackNavigationProp< + RootStackParamList, + 'ProjectNavigator' +> + export function ProjectNavigator({ route }: { route: ProjectStackRoute }) { return ( - - - - - - + + + + + + + + + + + ) } diff --git a/src/pages/SelectProject.tsx b/src/pages/SelectProject.tsx index 10d3cf7..64fb4b5 100644 --- a/src/pages/SelectProject.tsx +++ b/src/pages/SelectProject.tsx @@ -5,7 +5,7 @@ import { List } from 'react-native-paper' import { Environment } from 'wollok-ts/dist/model' import { stylesheet } from '../components/entities/Entity/styles' import FabAddScreen from '../components/FabScreens/FabAddScreen' -import { NewProjectModal } from '../components/select-project/NewProjectModal' +import { NewProjectModal } from '../components/projects/NewProjectModal' import { templateProject } from '../context/initialProject' import { loadProject, @@ -13,12 +13,13 @@ import { saveProject, } from '../services/persistance.service' import { useTheme } from '../theme' +import { ProjectScreenNavigationProp } from './ProjectNavigator' export function SelectProject() { const [projects, setProjects] = useState([]) const [showNewProjectModal, setShowNewProjectModal] = useState(false) const focused = useIsFocused() - const navigation = useNavigation() + const navigation = useNavigation() const theme = useTheme() diff --git a/src/pages/Describes.tsx b/src/pages/tabs/Describes.tsx similarity index 73% rename from src/pages/Describes.tsx rename to src/pages/tabs/Describes.tsx index 4493ce6..4100bee 100644 --- a/src/pages/Describes.tsx +++ b/src/pages/tabs/Describes.tsx @@ -1,10 +1,10 @@ import React, { useState } from 'react' import { ScrollView } from 'react-native-gesture-handler' import { Describe, Package } from 'wollok-ts/dist/model' -import FabAddScreen from '../components/FabScreens/FabAddScreen' -import NewDescribeModal from '../components/tests/NewDescribeModal' -import DescribeItem from '../components/tests/DescribeItem' -import { useProject } from '../context/ProjectProvider' +import FabAddScreen from '../../components/FabScreens/FabAddScreen' +import NewDescribeModal from '../../components/tests/NewDescribeModal' +import DescribeItem from '../../components/tests/DescribeItem' +import { useProject } from '../../context/ProjectProvider' export function Describes() { const { diff --git a/src/pages/Entities/Entities.tsx b/src/pages/tabs/Modules.tsx similarity index 97% rename from src/pages/Entities/Entities.tsx rename to src/pages/tabs/Modules.tsx index a9262a4..5c72f03 100644 --- a/src/pages/Entities/Entities.tsx +++ b/src/pages/tabs/Modules.tsx @@ -6,7 +6,7 @@ import NewEntityModal from '../../components/entities/NewEntityModal/NewEntityMo import FabAddScreen from '../../components/FabScreens/FabAddScreen' import { mainPackageName, useProject } from '../../context/ProjectProvider' -export function Entities() { +export function Modules() { const { project, actions: { addEntity }, diff --git a/src/services/persistance.service.ts b/src/services/persistance.service.ts index 370098a..5ae87be 100644 --- a/src/services/persistance.service.ts +++ b/src/services/persistance.service.ts @@ -5,7 +5,8 @@ import { writeFile, } from 'react-native-fs' import RNFetchBlob from 'rn-fetch-blob-v2' -import { Environment, fromJSON } from 'wollok-ts/dist/model' +import { Environment } from 'wollok-ts/dist/model' +import { fromJSON } from 'wollok-ts/dist/jsonUtils' import { projectToJSON } from '../utils/wollok-helpers' const projectsFolder = 'projects' diff --git a/src/utils/test-helpers.tsx b/src/utils/test-helpers.tsx deleted file mode 100644 index fb9afa8..0000000 --- a/src/utils/test-helpers.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { NavigationContainer } from '@react-navigation/native' -import { render } from '@testing-library/react-native' -import React from 'react' -import { theme } from '../theme' -import { OneOrMany } from './type-helpers' - -export function renderWithTheme(children: OneOrMany) { - return render( - {children}, - ) -} diff --git a/src/utils/type-helpers.ts b/src/utils/type-helpers.ts index fadabfe..174fd7d 100644 --- a/src/utils/type-helpers.ts +++ b/src/utils/type-helpers.ts @@ -9,3 +9,8 @@ export type Maybe = T | undefined export type ParentComponentProp = T & { children: OneOrMany } + +export type Visible = { + visible: boolean + setVisible: (value: boolean) => void +} diff --git a/src/utils/wollok-helpers.ts b/src/utils/wollok-helpers.ts index f209c9f..76e6f63 100644 --- a/src/utils/wollok-helpers.ts +++ b/src/utils/wollok-helpers.ts @@ -1,6 +1,7 @@ // TODO: import form Wollok // All these funtions are duplicated from Wollok import { upperCaseFirst } from 'upper-case-first' +import { List } from 'wollok-ts/dist/extensions' import interpret, { DirectedInterpreter, ExecutionDirector, @@ -14,13 +15,13 @@ import { Expression, Field, is, - List, Literal, Method, Module, Name, Node, Parameter, + Problem, Singleton, Test, Variable, @@ -56,10 +57,6 @@ export function methodLabel(method: Method): string { return `${method.name}(${method.parameters.map(_ => _.name).join(',')})` } -export function entityMemberLabel(node: EntityMemberWithBody): string { - return node.is('Method') ? methodLabel(node) : node.name -} - export function literalClassFQN(literal: Literal): Name { return `wollok.lang.${upperCaseFirst(typeof literal.value)}` } @@ -67,15 +64,17 @@ export function literalClassFQN(literal: Literal): Name { export function allScopedVariables( node: EntityMemberWithBody, ): Referenciable[] { - const fields = allFields(node.parent()) + const fields = allFields(node.parent) const params = node.is('Method') ? node.parameters : [] const methodVars = allVariables(node) return [...fields, ...params, ...methodVars] } +// METHODS + export function methodFQN(method: Method) { - return `${method.parent().fullyQualifiedName()}.${method.name}/${ + return `${method.parent.fullyQualifiedName()}.${method.name}/${ method.parameters.length }` } @@ -98,6 +97,31 @@ export function methodByFQN(environment: Environment, fqn: Name): Method { return entity.lookupMethod(methodName, Number.parseInt(methodArity, 10))! } +export function entityMemberLabel(node: EntityMemberWithBody): string { + return node.is('Method') ? methodLabel(node) : node.name +} + +export function entityMemberFQN(node: EntityMemberWithBody): string { + return node.is('Method') ? methodFQN(node) : node.fullyQualifiedName() +} + +export function entityMemberByFQN( + environment: Environment, + fqn: Name, +): EntityMemberWithBody { + return isMethodFQN(fqn) + ? methodByFQN(environment, fqn) + : environment.getNodeByFQN(fqn) +} + +// PROBLEMS + +export function isError(problem: Problem): boolean { + return problem.level === 'error' +} + +// TESTS + export type TestResult = 'Passed' | 'Failure' | 'Error' export type TestRun = { result: TestResult; exception?: WollokException } export function interpretTest(test: Test, environment: Environment): TestRun { diff --git a/translations/en.json b/translations/en.json index f8707d3..560c921 100644 --- a/translations/en.json +++ b/translations/en.json @@ -70,5 +70,10 @@ "selectProject": "Select a project", "newProject": "New project", "saved": "Saved!" + }, + "problem": { + "shouldInitializeAllAttributes": "All object attributes should be initialized", + "nameShouldBeginWithLowercase": "This name should start with lowercase", + "shouldNotDefineUnusedVariables": "This variable is never used" } } diff --git a/translations/es.json b/translations/es.json index 50797fa..70b6c29 100644 --- a/translations/es.json +++ b/translations/es.json @@ -71,5 +71,10 @@ "selectProject": "Seleccioná un proyecto", "newProject": "Proyecto nuevo", "saved": "¡Guardado!" + }, + "problem": { + "shouldInitializeAllAttributes": "Todos los atributos del objeto deben estar inicializados", + "nameShouldBeginWithLowercase": "Este nombre debería empezar con minúscula", + "shouldNotDefineUnusedVariables": "Esta variable no se está usando" } } diff --git a/yarn.lock b/yarn.lock index f0d4423..5b9098f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9544,9 +9544,9 @@ unpipe@~1.0.0: resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= -unraw@^2.0.0: +unraw@^2.0.1: version "2.0.1" - resolved "https://registry.npmjs.org/unraw/-/unraw-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/unraw/-/unraw-2.0.1.tgz#7b51dcdfb1e43d59d5e52cdb44d349d029edbaba" integrity sha512-tdOvLfRzHolwYcHS6HIX860MkK9LQ4+oLuNwFYL7bpgTEO64PZrcQxkisgwJYCfF8sKiWLwwu1c83DvMkbefIQ== unset-value@^1.0.0: @@ -9829,14 +9829,14 @@ widest-line@^2.0.0: dependencies: string-width "^2.1.1" -wollok-ts@^3.0.6: - version "3.0.6" - resolved "https://registry.npmjs.org/wollok-ts/-/wollok-ts-3.0.6.tgz" - integrity sha512-2TgwRhu0+lUgCxULz4ZjMlw8zfv1gKFE7v6eh4fNtVWBdlKozqS6QT3YwNPzbgPolTXW9xrMaAf1lxpsD3ye1Q== +wollok-ts@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/wollok-ts/-/wollok-ts-3.0.8.tgz#dd4bcf5ec2fa323a565e8f8688291b3a61f3765e" + integrity sha512-oBVEPL+MwCVsfoBnN3bLvYsr9vM9OmPdReNQq+sYM0h/l2cn2iYfD0xQdVC9u0hhrqpzfk+es63fUKAVLEkLJQ== dependencies: "@types/parsimmon" "^1.10.6" parsimmon "^1.18.0" - unraw "^2.0.0" + unraw "^2.0.1" uuid "8.3.2" word-wrap@^1.2.3, word-wrap@~1.2.3: