diff --git a/.babelrc b/.babelrc index ac762e007..f4175971b 100644 --- a/.babelrc +++ b/.babelrc @@ -13,8 +13,8 @@ ], // presets are a set of of plug-ins "presets": [ + ["@babel/preset-env", { "targets": { "node": "current" } }], "@babel/preset-typescript", - "@babel/preset-env", "@babel/preset-react" ] } diff --git a/.eslintrc.json b/.eslintrc.json index f621932e3..9b85387af 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -8,7 +8,7 @@ "sourceType": "module" }, "plugins": ["import", "react", "jest", "jsx-a11y", "babel"], - "parser": "babel-eslint", + "parser": "@babel/eslint-parser", "env": { "browser": true, "node": true, diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f937a06b2..ebb780741 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,9 +6,9 @@ name: CI on: # Triggers the workflow on push or pull request events but only for the master branch push: - branches: [ master ] + branches: [dev] pull_request: - branches: [ master ] + branches: [dev] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -23,11 +23,15 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 + with: + node-version: 18.13.0 # Runs a single command using the runners shell - - name: Run a one-line script - run: echo Hello, world! + - name: Install dependencies + run: npm install + - name: Run all tests + run: npm run test --bail # Runs a set of commands using the runners shell - name: Run a multi-line script diff --git a/.gitignore b/.gitignore index 086545ac6..fc94e8869 100644 --- a/.gitignore +++ b/.gitignore @@ -498,3 +498,6 @@ server/RootCA.srl # End of https://www.gitignore.io/api/node,linux,macos,windows,visualstudio,yarn +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/.travis.yml b/.travis.yml index df8e4d892..6d4e18982 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,17 @@ -language: node_js -node_js: - - "16" -dist: xenial -arch: amd64 -script: - - python3 -VV - - pip install --upgrade pip - - pip -V - - npm run test -cache: - directories: - - node_modules -install: - - npm install --legacy-peer-deps -env: - global: PATH=/opt/python/3.7.1/bin:$PATH +# language: node_js +# node_js: +# - "16" +# dist: xenial +# arch: amd64 +# script: +# - python3 -VV +# - pip install --upgrade pip +# - pip -V +# - npm run test +# cache: +# directories: +# - node_modules +# install: +# - npm install --legacy-peer-deps +# env: +# global: PATH=/opt/python/3.7.1/bin:$PATH diff --git a/CHANGE_LOG.md b/CHANGE_LOG.md index e73940be7..f8f5c7778 100644 --- a/CHANGE_LOG.md +++ b/CHANGE_LOG.md @@ -3,6 +3,56 @@

ReacType Change Log

+**Version 16.0.0 Changes** + +Changes:
+ +- Developer Improvements: + - Testing Coverage: + - Version 16 introduces end-to-end testing with Playwright and adds additional unit testing with React Testing Library. + - Testing coverage has now doubled since version 15, and now sits at just over 50% coverage. + - Transitioned away from Enzyme to maintain consistency with RTL and Jest. + - Typescript Conversion: + - Upped typescript coverage from 30% to 80%. + - Fixed multiple type errors in component files. + - Added CI pipeline for testing: + - Transitioned away from Travis CI to Github Actions for improved CI pipeline. Github Actions will now run all tests upon each pull request to dev. + - Updated OAuth and Sign In Features: + - Sign in feature now connected to the latest database version. + - Fixed bug that allowed only one OAuth user to sign in at a time. + - Github OAuth is now connected to Adam Vanek. + - Dev Bug Fixes: + - Debugged ‘worker error’ on code preview & style editor by refactoring Ace-Build components. + - Additional logic added for edge cases in inputs for context manager, state manager, and signup features. + - Cleaned up hundreds of lines of outdated code and deleted multiple unused and duplicate files + - Dependency Updates: + - All previously outdated dependencies are now updated. Time it takes for the app to bundle in dev is now cut in half. +- User Features: + - Export Button: + - Export feature on the web app now allows users to download the current project as a zip file with modularized component folder, html, and css file included. + - Export feature is now available to all users including guests. + - CSS Live Rendering: + - CSS Editor changes now rendered visually in the demo page on save. + - UI Changes: + - Fixed multiple contrast issues with white text displaying on white background in State Manger Display tab tables, state management tables, table menu dropdowns, Context Manager tables, and Context Manager display. + - Adjusted context manager interface for improved UX when creating context and saving key/value pairs. + - Fixed border styling within modals and error messages that were cutting off inputs on focus. + - Added save button to customization tab. + - Bug Fixes: + - Manage project features for registered users now successfully saves, opens, and deletes projects. + - State Manager now successfully deletes state from parent components. + - Context Manager display chart renders correctly. + - CSS Editor contents now persist after rerender. + +Recommendations for Future Enhancements:
+ +- Refactor away from MUI. MUI is very opinionated and while creating components with it is easy it leaves a lot to be desired. Dark Mode also needs to be improved as color contrast is very low. +- Continue expanding testing coverage. Improve testing by adding additional unit tests, expanding end-to-end testing, and introducing integration testing. +- Continue quality Typescript conversion. Continue to fix type errors within component files. +- Modularize appStateSlice file. Further modularization is needed for readability and maintainability. +- Solve residual bugs. Undo & Redo buttons on customization page not functioning as expected. Backend bugs persist as seen in the console when running the dev environment. Resolve electron app functionality to coincide with web app functionality. +- Continue code cleanup. Continue cleanup of outdated and unused code and files + **Version 15.0.0 Changes** Changes:
diff --git a/README.md b/README.md index 74154ba41..233f68833 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@

- +

**ReacType** is a rapid prototyping tool that allows users _visualize_ their application architecture dynamically, employing a _drag-and-drop canvas display_ and an interactive, _real-time component code preview_ that can be exported as a **React** app for developers employing React component architecture alongside the comprehensive type-checking of **TypeScript**. In other words, **you can draw prototypes and export React / TypeScript code!** @@ -40,12 +40,14 @@ Follow [@ReacType](https://twitter.com/reactype) on Twitter for important announ If you want to read about using ReacType, the [User Manual](https://reactype-1.herokuapp.com/#/tutorial) is free and available online now. -## Changes with version 15.0.0 +## Changes with version 16.0.0 -- **Consolidated State Management**: Reactype was using mutliple methods of State Management. We consolidated this all to the modern version of Redux Toolkit to enhance the developer experience and make it easier to debug. -- **Major Dependency Updates**: No more workarounds required to get new developers up and running in the project. A simple npm install works as intended. -- **Websockets**: Users can now join rooms to collaborate on a project together in real time! -- **Fully Deployed Web Application**: To utilize these new features we have hosted the full application via AWS so users can easily use Reactype without a download. +- **Improved Testing Coverage**: Testing coverage has now doubled since version 15, and now sits at just over 50% coverage. Version 16 introduces end-to-end testing with Playwright and adds additional unit testing with React Testing Library. +- **Major Bug Fixes**: Manage Project Features now work as expected. State Manager now deletes state from parent components. Context Manager Display Tab and CSS Editor now rendering as expected. +- **Typescript Conversion**: Typescript coverage has improved from 30% to 80% with additional interfaces added for quality improvements. +- **Live CSS Demo Rendering**: CSS Editor changes now rendered visually in the demo page on save. +- **Universal Exports on Web App**: Export feature on web app now allows users to download the current project as a zip file with modularized component folder, html, and css file included. Export feature is now available to all users including guests. +- **UI Improvements**: Fixed multiple contrast issues with white text displaying on white background. Adjusted context manager interface for improved UX. Fixed border styling within modals and error messages. - **And more:** See [change log](https://github.com/open-source-labs/ReacType/blob/master/CHANGE_LOG.md) for more details on what was changed from the previous versions as well as plans for upcoming features! ## File Structure courtesy of Reactype version 14.0.0 diff --git a/__tests__/BottomTabs.test.tsx b/__tests__/BottomTabs.test.tsx index f2d9cc525..ec2be92df 100644 --- a/__tests__/BottomTabs.test.tsx +++ b/__tests__/BottomTabs.test.tsx @@ -1,29 +1,231 @@ -import React, { useReducer} from 'react'; +import React from 'react'; +import { Provider } from 'react-redux'; import '@testing-library/jest-dom'; -import { render, screen } from '@testing-library/react'; - +import { + render, + screen, + fireEvent, + waitFor, + within +} from '@testing-library/react'; import BottomTabs from '../app/src/components/bottom/BottomTabs'; -import StateContext from '../app/src/context/context'; -import initialState from '../app/src/context/initialState'; -import reducer from '../app/src/reducers/componentReducer'; -function Test() { - const [state, dispatch] = useReducer(reducer, initialState); - return ( - - - - ) -} -test('Bottom Panel Contains Two Tabs: Code Preview and Component Tree', () => { - render(); - expect(screen.getAllByRole('tab')).toHaveLength(7); - expect(screen.getByText('Code Preview')).toBeInTheDocument(); - expect(screen.getByText('Component Tree')).toBeInTheDocument(); - expect(screen.getByText('Creation Panel')).toBeInTheDocument(); - expect(screen.getByText('Customization')).toBeInTheDocument(); - expect(screen.getByText('CSS Editor')).toBeInTheDocument(); - expect(screen.getByText('Code Preview')).toBeInTheDocument(); - expect(screen.getByText('Component Tree')).toBeInTheDocument(); - expect(screen.getByText('Context Manager')).toBeInTheDocument(); - expect(screen.getByText('State Manager')).toBeInTheDocument(); -}) +import ContextManager from '../app/src/components/ContextAPIManager/ContextManager'; +import store from '../app/src/redux/store'; +import ComponentPanel from '../app/src/components/right/ComponentPanel'; +import HTMLPanel from '../app/src/components/left/HTMLPanel'; +import StateManager from '../app/src/components/StateManagement/StateManagement'; +import CustomizationPanel from '../app/src/containers/CustomizationPanel'; +import { BrowserRouter } from 'react-router-dom'; +import DragDropPanel from '../app/src/components/left/DragDropPanel'; +import MainContainer from '../app/src/containers/MainContainer'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; + +describe('Bottom Panel Render Test', () => { + test('should render all seven tabs', () => { + render( + + + + ); + expect(screen.getAllByRole('tab')).toHaveLength(7); + expect(screen.getByText('Code Preview')).toBeInTheDocument(); + expect(screen.getByText('Component Tree')).toBeInTheDocument(); + expect(screen.getByText('Creation Panel')).toBeInTheDocument(); + expect(screen.getByText('Customization')).toBeInTheDocument(); + expect(screen.getByText('CSS Editor')).toBeInTheDocument(); + expect(screen.getByText('Context Manager')).toBeInTheDocument(); + expect(screen.getByText('State Manager')).toBeInTheDocument(); + }); +}); + +describe('Creation Panel', () => { + test('should invalidate empty field in New Component name', async () => { + render( + + + + ); + + fireEvent.click(screen.getByText('Create')); + + await waitFor(() => { + expect( + screen.getByText('Component name cannot be blank.') + ).toBeInTheDocument(); + }); + }); + + test('should invalidate New Component name containing symbols', async () => { + render( + + + + ); + + fireEvent.change(screen.getByLabelText('Name:'), { + target: { + value: '!@#' + } + }); + + fireEvent.click(screen.getByText('Create')); + + await waitFor(() => { + expect( + screen.getByText('Component name must start with a letter.') + ).toBeInTheDocument(); + }); + }); + + test('should invalidate empty field in HTML Tag tag', async () => { + render( + + + + ); + + fireEvent.click(screen.getByText('Add Element')); + + await waitFor(() => { + expect(screen.getAllByText('* Input cannot be blank. *')).toHaveLength(2); + }); + }); + + test('should invalidate HTML Element name containing symbols', async () => { + render( + + + + ); + + fireEvent.change(screen.getByLabelText('Element Name:'), { + target: { + value: '!@#' + } + }); + + fireEvent.change(screen.getByLabelText('Tag:'), { + target: { + value: '!@#' + } + }); + + fireEvent.click(screen.getByText('Add Element')); + + await waitFor(() => { + expect( + screen.getAllByText('* Input must start with a letter. *') + ).toHaveLength(2); + }); + }); +}); + +describe('Context Manager', () => { + test('should render Create/Edit, Assign, and Display tabs', () => { + render( + + + + ); + expect(screen.getAllByRole('tab')).toHaveLength(3); + }); + test('Create/Edit Tab should contain all buttons, inputs field, and a data table', () => { + render( + + + + ); + expect(screen.getAllByRole('textbox')).toHaveLength(3); + expect(screen.getAllByRole('button')).toHaveLength(4); + expect(screen.getByText('Context Name')).toBeInTheDocument(); + expect(screen.getByRole('table')).toBeInTheDocument(); + }); + test('Assign Tab should contain all buttons and input fields', () => { + render( + + + + ); + + fireEvent.click(screen.getByText('Assign')); + expect(screen.getByText('Contexts Consumed')).toBeInTheDocument(); + const dropdown = screen.getByLabelText('Select Component'); + expect(dropdown).toBeInTheDocument(); + expect(screen.getAllByRole('button')).toHaveLength(1); + expect(screen.getAllByRole('combobox')).toHaveLength(2); + expect(screen.getAllByRole('table')).toHaveLength(2); + }); +}); + +describe('State Manager', () => { + test('Should render all containers', () => { + render( + + + + ); + expect(screen.getAllByRole('heading')).toHaveLength(4); + expect(screen.getAllByRole('textbox')).toHaveLength(2); + expect(screen.getAllByRole('grid')).toHaveLength(3); + expect(screen.getAllByRole('columnheader')).toHaveLength(9); + }); + + test('Display tab should render correct elements', () => { + render( + + + + ); + fireEvent.click(screen.getByText('Display')); + expect(screen.getByRole('table')).toBeInTheDocument(); + expect( + screen.getByText('State Initialized in Current Component:') + ).toBeInTheDocument(); + }); +}); + +describe('Customization Panel', () => { + test('Should render customization container with no elements in Canvas', () => { + render( + + + + + + ); + expect(screen.getByText('Parent Component:')).toBeInTheDocument(); + expect(screen.getByText('App')).toBeInTheDocument(); + expect( + screen.getByText( + 'Drag and drop an html element (or focus one) to see what happens!' + ) + ).toBeInTheDocument(); + }); + test('Should render all buttons and inputs when Canvas has element', () => { + render( + + + + + + + + + + ); + const drop = screen.getByTestId('drop'); + const div = screen.getAllByText('Div')[0]; + expect(drop).toBeInTheDocument(); + expect(div).toBeInTheDocument(); + fireEvent.dragStart(div); + fireEvent.dragEnter(drop); + fireEvent.dragOver(drop); + fireEvent.drop(drop); + //check if customization panel elements are rendering correctly + const panel = screen.getByTestId('customization'); + expect(within(panel).getAllByRole('textbox')).toHaveLength(4); + // check dropdowns + expect(within(panel).getAllByRole('button')).toHaveLength(12); + }); +}); diff --git a/__tests__/DragAndDrop.test.tsx b/__tests__/DragAndDrop.test.tsx new file mode 100644 index 000000000..9431a2b43 --- /dev/null +++ b/__tests__/DragAndDrop.test.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import '@testing-library/jest-dom'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import { fireEvent, render, screen } from '@testing-library/react'; +import DragDropPanel from '../app/src/components/left/DragDropPanel'; +import ComponentDrag from '../app/src/components/left/ComponentDrag'; +import { Provider } from 'react-redux'; +import store from '../app/src/redux/store'; +import MainContainer from '../app/src/containers/MainContainer'; +import { within } from '@testing-library/react'; + + +function TestContext(component) { + return ( + + {component} + + ); +} + +describe('Drag and Drop Side Panel', () => { + test('Renders all HTML Element choices', () => { + render(TestContext()); + expect(screen.getByText('Div')).toBeInTheDocument(); + expect(screen.getByText('Img')).toBeInTheDocument(); + expect(screen.getByText('Form')).toBeInTheDocument(); + expect(screen.getByText('Button')).toBeInTheDocument(); + expect(screen.getByText('Link')).toBeInTheDocument(); + expect(screen.getByText('Paragraph')).toBeInTheDocument(); + expect(screen.getByText('Header 1')).toBeInTheDocument(); + expect(screen.getByText('Header 2')).toBeInTheDocument(); + expect(screen.getByText('Span')).toBeInTheDocument(); + expect(screen.getByText('Input')).toBeInTheDocument(); + expect(screen.getByText('Label')).toBeInTheDocument(); + expect(screen.getByText('Ordered List')).toBeInTheDocument(); + expect(screen.getByText('Unordered List')).toBeInTheDocument(); + expect(screen.getByText('Menu')).toBeInTheDocument(); + expect(screen.getByText('List')).toBeInTheDocument(); + expect(screen.queryByText('separator')).toBe(null); + }); + + test('Renders all React Router Component choices', () => { + render(TestContext()); + + expect(screen.getByText('Switch')).toBeInTheDocument(); + expect(screen.getByText('Route')).toBeInTheDocument(); + expect(screen.getByText('LinkTo')).toBeInTheDocument(); + }); + + test('Should render Roots Components and Reusbale components', () => { + render(TestContext()); + + expect(screen.getByText('Root Components')).toBeInTheDocument(); + expect(screen.getByText('Reusable Components')).toBeInTheDocument(); + }); + test('test drag and drop', () => { + render( + TestContext( + <> + + + + ) + ); + const drop = screen.getByTestId('drop'); + const div = screen.getByText('Div'); + expect(drop).toBeInTheDocument(); + expect(div).toBeInTheDocument(); + fireEvent.dragStart(div); + fireEvent.dragEnter(drop); + fireEvent.dragOver(drop); + fireEvent.drop(drop); + expect(within(drop).getByText('div')).toBeInTheDocument(); + }); +}); diff --git a/__tests__/HTMLPanel.test.tsx b/__tests__/HTMLPanel.test.tsx deleted file mode 100644 index b68403650..000000000 --- a/__tests__/HTMLPanel.test.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React, { useReducer} from 'react'; -import '@testing-library/jest-dom'; -import { DndProvider } from 'react-dnd'; -import { HTML5Backend } from 'react-dnd-html5-backend'; -import { render, fireEvent, cleanup, screen } from '@testing-library/react'; -import StateContext from '../app/src/context/context'; -import initialState from '../app/src/context/initialState'; -import reducer from '../app/src/reducers/componentReducer'; -import HTMLPanel from '../app/src/components/left/HTMLPanel'; -function Test() { - const [state, dispatch] = useReducer(reducer, initialState); - return ( - - - - - - ) -} -xtest('Renders HTMLPanel component properly', () => { - render( - - ); - expect(screen.getAllByRole('textbox')).toHaveLength(2); - expect(screen.getByText('Div')).toBeInTheDocument(); - expect(screen.getByText('Image')).toBeInTheDocument(); - expect(screen.getByText('Form')).toBeInTheDocument(); - expect(screen.getByText('List')).toBeInTheDocument(); - expect(screen.getByText('Button')).toBeInTheDocument(); - expect(screen.getByText('Link')).toBeInTheDocument(); - expect(screen.getByText('Paragraph')).toBeInTheDocument(); - expect(screen.getByText('Header 1')).toBeInTheDocument(); - expect(screen.getByText('Header 2')).toBeInTheDocument(); - expect(screen.getByText('Span')).toBeInTheDocument(); - expect(screen.queryByText('separator')).toBe(null); -}); - -xtest('Adds new custom element', () => { - render( - - ); - fireEvent.change(screen.getAllByRole('textbox')[0], { - target: { value: 'Testing' } - }); - fireEvent.change(screen.getAllByRole('textbox')[1], { - target: { value: 'Testing' } - }); - fireEvent.click(screen.getByDisplayValue('Add Element')); - expect(screen.getByText('Testing')).toBeInTheDocument(); -}); diff --git a/__tests__/__snapshots__/enzyme.test.tsx.snap b/__tests__/__snapshots__/enzyme.test.tsx.snap deleted file mode 100644 index 3a19cebd2..000000000 --- a/__tests__/__snapshots__/enzyme.test.tsx.snap +++ /dev/null @@ -1,713 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Test HTMLPanel Component Matches snapshot 1`] = ` - - Drag in a component or HTML element into the canvas!", - "future": Array [], - "id": 1, - "isPage": true, - "name": "App", - "past": Array [], - "stateProps": Array [], - "style": Object {}, - "useStateCodes": Array [], - }, - ], - "config": Object { - "saveFlag": true, - "saveTimer": false, - }, - "isLoggedIn": false, - "name": "", - "nextChildId": 1, - "nextComponentId": 2, - "nextTopSeparatorId": 1000, - "projectType": "Classic React", - "rootComponents": Array [ - 1, - ], - } - } - > - - - -`; - -exports[`Test the BottomTabs component Matches snapshot 1`] = ` -
- - - - - - - - - - -
- - - - Classic React - - - Gatsby.js - - - Next.js - - - -
-
-
- -
-
-`; - -exports[`Test the CanvasContainer component Matches snapshot 1`] = ` -
- -
-`; diff --git a/__tests__/componentReducer.test.ts b/__tests__/componentReducer.test.ts index ccf9f0987..72f736449 100644 --- a/__tests__/componentReducer.test.ts +++ b/__tests__/componentReducer.test.ts @@ -1,65 +1,76 @@ -import reducer from '../app/src/reducers/componentReducer'; +import reducer from '../app/src/redux/reducers/slice/appStateSlice'; import { State, Action } from '../app/src/interfaces/Interfaces'; +import { initialState } from '../app/src/redux/reducers/slice/appStateSlice'; -import initialState from '../app/src/context/initialState'; -import { iterate } from 'localforage'; - -describe('Testing componentReducer functionality', () => { +describe('componentReducer Test', () => { let state: State = initialState; // TEST 'ADD COMPONENT' - describe('ADD COMPONENT reducer', () => { + describe('addComponent reducer', () => { it('should add new reuseable component to state', () => { const action: Action = { - type: 'ADD COMPONENT', + type: 'appState/addComponent', payload: { componentName: 'TestRegular', root: false, - }, + contextParam: { + allContext: [] + } + } }; state = reducer(state, action); // expect state.components array to have length 2 const length = state.components.length; expect(length).toEqual(2); // expect new component name to match name of last elem in state.components array - expect(state.components[length - 1].name).toEqual(action.payload.componentName); + expect(state.components[length - 1].name).toEqual( + action.payload.componentName + ); }); }); - // TEST 'ADD COMPONENT' with new root - describe('ADD COMPONENT reducer', () => { + // TEST 'ADD COMPONENT' with new root + describe('addComponent', () => { it('should add new reuseable component to state as the new root', () => { const action: Action = { - type: 'ADD COMPONENT', + type: 'appState/addComponent', payload: { componentName: 'TestRootChange', id: 3, root: true, - }, + contextParam: { + allContext: [] + } + } }; state = reducer(state, action); - + // expect state.components array to have length 3 const length = state.components.length; expect(length).toEqual(3); // expect new root to match id of component id of TestRootChange (rootComponents is an array of component ID numbers) - expect(state.rootComponents[state.rootComponents.length - 1]).toEqual(action.payload.id); + expect(state.rootComponents[state.rootComponents.length - 1]).toEqual( + action.payload.id + ); }); }); // TEST 'ADD CHILD' - describe('ADD CHILD reducer', () => { + describe('addChild', () => { it('should add child component and separator to top-level component', () => { const action: Action = { - type: 'ADD CHILD', + type: 'appState/addChild', payload: { type: 'Component', typeId: 2, childId: null, - }, + contextParam: { + allContext: [] + } + } }; // switch focus to very first root component - state.canvasFocus = { componentId: 1, childId: null }; + // state.canvasFocus = { componentId: 1, childId: null }; state = reducer(state, action); const newParent = state.components[0]; // expect new parent's children array to have length 2 (component + separator) @@ -68,67 +79,116 @@ describe('Testing componentReducer functionality', () => { expect(newParent.children[0].name).toEqual('separator'); // expect new child to have type 'Component' expect(newParent.children[1].type).toEqual('Component'); - const addedChild = state.components.find(comp => comp.id === newParent.children[1].typeId); + const addedChild = state.components.find( + (comp) => comp.id === newParent.children[1].typeId + ); // expect new child typeId to correspond to component with name 'TestRegular' expect(addedChild.name).toEqual('TestRegular'); }); }); // TEST 'CHANGE POSITION' - describe('CHANGE POSITION reducer ', () => { + describe('changePosition', () => { it('should move position of an instance', () => { const actionHtml: Action = { - type: 'ADD CHILD', + type: 'appState/addChild', payload: { type: 'HTML Element', typeId: 9, childId: null, - }, + contextParam: { + allContext: [] + } + } }; state = reducer(state, actionHtml); const actionChangePos: Action = { - type: 'CHANGE POSITION', + type: 'appState/changePosition', payload: { currentChildId: 1, newParentChildId: null, - }, + contextParam: { + allContext: [] + } + } }; + state = reducer(state, actionChangePos); - const changeParent = state.components.find(comp => comp.id === state.canvasFocus.componentId); + const changeParent = state.components.find( + (comp) => comp.id === state.canvasFocus.componentId + ); const changeParentChildLength = changeParent.children.length; // expect last child of parent to be moved Component element - expect(changeParent.children[changeParentChildLength - 1].type).toEqual('Component'); + expect(changeParent.children[changeParentChildLength - 1].type).toEqual( + 'Component' + ); // expect last child of parent to have current child ID of payload - expect(changeParent.children[changeParentChildLength - 1].childId).toEqual(1); + expect( + changeParent.children[changeParentChildLength - 1].childId + ).toEqual(1); + }); + }); + + // TEST 'UPDATE CSS' + describe('updateCss', () => { + it('should add style to focused component', () => { + const action: Action = { + type: 'appState/updateCss', + payload: { + style: { + backgroundColor: 'gray' + }, + contextParam: { + allContext: [] + } + } + }; + state = reducer(state, action); + // expect the style property on targeted comp to equal style property in payload + expect(state.components[0].children[1].style).toEqual( + action.payload.style + ); }); }); // TEST 'DELETE CHILD' - describe('DELETE CHILD reducer', () => { + describe('deleteChild', () => { it('should delete child of focused top-level component', () => { // canvas still focused on childId: 2, which is an HTML element const action: Action = { - type: 'DELETE CHILD', + type: 'appState/deleteChild', + payload: { + id: 2, + contextParam: { + allContext: [] + } + } }; state = reducer(state, action); // expect only one remaining child - const delParent = state.components.find(comp => comp.id === state.canvasFocus.componentId); + const delParent = state.components.find( + (comp) => comp.id === state.canvasFocus.componentId + ); // expect remaining child to have type 'Component' and to be preceded by separator expect(delParent.children.length).toEqual(2); - expect(delParent.children[delParent.children.length - 1].type).toEqual('Component'); - expect(delParent.children[delParent.children.length - 2].name).toEqual('separator'); + expect(delParent.children[delParent.children.length - 1].type).toEqual( + 'Component' + ); + expect(delParent.children[delParent.children.length - 2].name).toEqual( + 'separator' + ); }); }); // TEST 'CHANGE FOCUS' - describe('CHANGE FOCUS reducer', () => { + describe('changeFocus', () => { it('should change focus to specified component', () => { const action: Action = { - type: 'CHANGE FOCUS', + type: 'appState/changeFocus', payload: { componentId: 2, - childId: null, - }, + childId: null + } }; state = reducer(state, action); expect(state.canvasFocus.componentId).toEqual(2); @@ -136,30 +196,12 @@ describe('Testing componentReducer functionality', () => { }); }); - // TEST 'UPDATE CSS' - describe('UPDATE CSS reducer', () => { - it('should add style to focused component', () => { - const action: Action = { - type: 'UPDATE CSS', - payload: { - style: { - backgroundColor: 'gray', - }, - }, - }; - state = reducer(state, action); - const styledComp = state.components.find(comp => comp.id === state.canvasFocus.componentId); - // expect the style property on targeted comp to equal style property in payload - expect(styledComp.style.backgroundColor).toEqual(action.payload.style.backgroundColor); - }); - }); - // TEST 'UPDATE PROJECT NAME' - describe('UPDATE PROJECT NAME reducer', () => { + describe('updateProjectName', () => { it('should update project with specified name', () => { const action: Action = { - type: 'UPDATE PROJECT NAME', - payload: 'TESTNAME', + type: 'appState/updateProjectName', + payload: 'TESTNAME' }; state = reducer(state, action); // expect state name to equal payload @@ -168,22 +210,24 @@ describe('Testing componentReducer functionality', () => { }); // TEST 'CHANGE PROJECT TYPE' - describe('CHANGE PROJECT TYPE reducer', () => { + describe('changeProjectType', () => { it('should change project type to specified type', () => { const action: Action = { - type: 'CHANGE PROJECT TYPE', + type: 'appState/changeProjectType', payload: { projectType: 'Classic React', - }, + contextParam: { + allContext: [] + } + } }; state = reducer(state, action); expect(state.projectType).toEqual(action.payload.projectType); }); }); - // TEST 'UNDO' - describe('UNDO reducer', () => { + xdescribe('undo', () => { it('should remove the last element from the past array and push it to the future array', () => { const focusIndex = state.canvasFocus.componentId - 1; state.components[focusIndex].past = []; @@ -191,52 +235,56 @@ describe('Testing componentReducer functionality', () => { // snapShotFunc taken from src/components/main/canvas.tsx to test undo functionality // snapShotFunc takes a snapshot of state to be used in UNDO/REDO functionality const snapShotFuncCopy = () => { - const deepCopiedState = JSON.parse(JSON.stringify(state)); - // pushes the last user action on the canvas into the past array of Component - state.components[focusIndex].past.push(deepCopiedState.components[focusIndex].children); - } + const deepCopiedState = JSON.parse(JSON.stringify(state)); + // pushes the last user action on the canvas into the past array of Component + state.components[focusIndex].past.push( + deepCopiedState.components[focusIndex].children + ); + }; const actionHTML2: Action = { - type: 'ADD CHILD', + type: 'appState/addChild', payload: { type: 'HTML Element', typeId: 4, - childId: null, + childId: null } - } + }; state = reducer(state, actionHTML2); // invoking snapShotFunc is necessary to push actions into the past array, referenced in the UNDO functionality to define children snapShotFuncCopy(); const actionUndo: Action = { - type: 'UNDO', - payload: {}, + type: 'appState/undo', + payload: {} }; state = reducer(state, actionUndo); expect(state.components[focusIndex].past.length).toEqual(0); expect(state.components[focusIndex].future.length).toEqual(1); - }) + }); }); + // TEST 'REDO' - describe('REDO reducer', () => { + xdescribe('redo', () => { it('should remove the last element from the future array and push it to the past array', () => { const focusIndex = state.canvasFocus.componentId - 1; const actionRedo: Action = { - type: 'REDO', - payload: {}, + type: 'appState/redo', + payload: {} }; state = reducer(state, actionRedo); expect(state.components[focusIndex].future.length).toEqual(0); expect(state.components[focusIndex].past.length).toEqual(1); - }) + }); }); + // TEST 'RESET STATE' - describe('RESET STATE reducer', () => { + describe('resetState', () => { it('should reset project to initial state', () => { const action: Action = { - type: 'RESET STATE', - payload: '', + type: 'appState/resetState', + payload: '' }; state = reducer(state, action); // expect default project to have empty string as name @@ -248,4 +296,3 @@ describe('Testing componentReducer functionality', () => { }); }); }); - diff --git a/__tests__/contextReducer.test.js b/__tests__/contextReducer.test.js index 9a2f11dd4..72b98944f 100644 --- a/__tests__/contextReducer.test.js +++ b/__tests__/contextReducer.test.js @@ -1,11 +1,11 @@ -import subject from '../app/src/redux/reducers/slice/contextReducer'; +import subject from '../app/src/redux/reducers/slice/contextReducer.ts'; -describe('Context Reducer', () => { +describe('contextReducer test', () => { let state; beforeEach(() => { state = { - allContext: [] + allContext: [], }; }); @@ -21,12 +21,12 @@ describe('Context Reducer', () => { }); }); - describe('ADD_CONTEXT', () => { + describe('addContext', () => { const action = { - type: 'ADD_CONTEXT', + type: 'context/addContext', payload: { - name: 'Theme Context' - } + name: 'Theme Context', + }, }; it('adds a context', () => { @@ -34,7 +34,7 @@ describe('Context Reducer', () => { expect(allContext[0]).toEqual({ name: 'Theme Context', values: [], - components: [] + components: [], }); }); @@ -49,26 +49,56 @@ describe('Context Reducer', () => { }); }); - describe('ADD_CONTEXT_VALUES', () => { + // OLD ADD CONTEX TEST + + // describe('ADD_CONTEXT', () => { + // const action = { + // type: 'ADD_CONTEXT', + // payload: { + // name: 'Theme Context' + // } + // }; + + // it('adds a context', () => { + // const { allContext } = subject(state, action); + // expect(allContext[0]).toEqual({ + // name: 'Theme Context', + // values: [], + // components: [] + // }); + // }); + + // it('returns a state object not strictly equal to the original', () => { + // const newState = subject(state, action); + // expect(newState).not.toBe(state); + // }); + + // it('should immutably update the nested state object', () => { + // const { allContext } = subject(state, action); + // expect(allContext).not.toBe(state.allContext); + // }); + // }); + + describe('addContextValues', () => { beforeEach(() => { state = { allContext: [ { name: 'Theme Context', values: [], - components: [] - } - ] + components: [], + }, + ], }; }); const action = { - type: 'ADD_CONTEXT_VALUES', + type: 'context/addContextValues', payload: { name: 'Theme Context', inputKey: 'Theme Color', - inputValue: 'Dark' - } + inputValue: 'Dark', + }, }; it('adds a key-value pair to values array of the specified context', () => { @@ -85,7 +115,7 @@ describe('Context Reducer', () => { }); }); - describe('DELETE CONTEXT', () => { + describe('deleteContext', () => { let action; beforeEach(() => { state = { @@ -93,21 +123,21 @@ describe('Context Reducer', () => { { name: 'Theme Context', values: [], - components: [] + components: [], }, { name: 'To be deleted', values: [], - components: [] - } - ] + components: [], + }, + ], }; action = { - type: 'DELETE_CONTEXT', + type: 'context/deleteContext', payload: { - name: 'Theme Context' - } + name: 'Theme Context', + }, }; }); @@ -124,29 +154,29 @@ describe('Context Reducer', () => { }); }); - describe('ADD_COMPONENT_TO_CONTEXT', () => { + describe('addComponentToContext', () => { beforeEach(() => { state = { allContext: [ { name: 'Theme Context', values: [], - components: [] - } - ] + components: [], + }, + ], }; }); const action = { - type: 'ADD_COMPONENT_TO_CONTEXT', + type: 'context/addComponentToContext', payload: { context: { - name: 'Theme Context' + name: 'Theme Context', }, component: { - name: 'Main Component' - } - } + name: 'Main Component', + }, + }, }; it('adds a new component to the specified context', () => { diff --git a/__tests__/enzyme.test.tsx b/__tests__/enzyme.test.tsx deleted file mode 100644 index a963ed6ec..000000000 --- a/__tests__/enzyme.test.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import { shallow} from 'enzyme'; -// import { configure } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16' -import React from 'react'; -import { DndProvider } from 'react-dnd'; -import { Provider } from "react-redux"; -import { HTML5Backend } from 'react-dnd-html5-backend'; -import StateContext from '../app/src/context/context'; -import initialState from '../app/src/context/initialState'; -import MainContainer from '../app/src/containers/MainContainer'; -import BottomPanel from '../app/src/components/bottom/BottomPanel'; -import BottomTabs from '../app/src/components/bottom/BottomTabs'; -import CanvasContainer from '../app/src/components/main/CanvasContainer'; -import Canvas from '../app/src/components/main/Canvas'; -import HTMLPanel from '../app/src/components/left/HTMLPanel'; -import HTMLItem from '../app/src/components/left/HTMLItem'; -import LeftContainer from '../app/src/containers/LeftContainer'; -import AppContainer from '../app/src/containers/AppContainer'; -import NavBar from '../app/src/components/top/NavBar'; -import MenuItem from '@mui/material/MenuItem'; -import Tab from '@mui/material/Tab'; -import LoginButton from '../app/src/components/right/LoginButton'; -import customizationPanel from '../app/src/containers/CustomizationPanel' - -import configureMockStore from "redux-mock-store"; -const mockStore = configureMockStore(); -const store = mockStore({}); - -configure({ adapter: new Adapter() }) - -/* If there is an error with unmatched snapshots because of intentionally modified codes, delete the contents in enzyme.test.tsx.snap to record new codes as blueprints */ - -describe('Test the CanvasContainer component', () => { - const target = shallow(); - it('Matches snapshot', () => { - expect(target).toMatchSnapshot(); - }); - // test if Canvas component is rendered - it('Contains Canvas component', () => { - expect(target.contains()).toBe(true); - }); -}); - -describe('Test the MainContainer component', () => { - const target = shallow(); - // test it canvas container is rendered - it('Contains CanvasContainer component', () => { - expect(target.contains()).toBe(true); - }); - // test if bottom panel is rendered - it('Contains BottomPanel component', () => { - expect(target.contains()).toBe(true); - }); -}); - -describe('Test the BottomTabs component', () => { - const target = shallow(); - it('Matches snapshot', () => { - expect(target).toMatchSnapshot(); - }); - // test if bottom tab has a Code Preview and a Component Tree button - it('Has two tabs called "Code Preview" and "Component Tree" ', () => { - expect(target.find(Tab)).toHaveLength(7); - expect(target.find(Tab).at(0).prop('label')).toEqual('Creation Panel'); - expect(target.find(Tab).at(1).prop('label')).toEqual('Customization'); - expect(target.find(Tab).at(2).prop('label')).toEqual('CSS Editor'); - expect(target.find(Tab).at(3).prop('label')).toEqual('Code Preview'); - expect(target.find(Tab).at(4).prop('label')).toEqual('Component Tree'); - expect(target.find(Tab).at(5).prop('label')).toEqual('Context Manager'); - expect(target.find(Tab).at(6).prop('label')).toEqual('State Manager'); - }); - // test if the dropdown menu exists on the bottom tab - it('Has a dropdown selection menu for Classic React, Gatsby.js, and Next.js', () => { - expect(target.find(MenuItem)).toHaveLength(3); - expect(target.find(MenuItem).at(0).text()).toEqual('Classic React'); - expect(target.find(MenuItem).at(1).text()).toEqual('Gatsby.js'); - expect(target.find(MenuItem).at(2).text()).toEqual('Next.js'); - }); -}); -// test the drag and drop component in the left panel -describe('Test HTMLPanel Component', () => { - const target = shallow( - - - - - - ); - - const props = { - name: 'abc', - key:'html-abc', - id:1, - Icon:'icon', - handleDelete: jest.fn() - }; - - it('Matches snapshot', () => { - expect(target).toMatchSnapshot(); - }); - // test if there are html items such as form, img, etc. on the left side - it('Should render HTMLItem', () => { - expect(target.find()).toBeDefined(); -}); -}); - -//testing for AppContainer - had to comment out - ahsan -describe('Test AppContainer container', () => { - const target = shallow( - - - - ); - - const props = { - setTheme: jest.fn(), - isThemeLight: jest.fn(), - }; - - // testing if there is a NavBar - it('Should render NavBar', () => { - expect( - target.find( - - ) - ).toBeDefined(); - }); - // testing for a RightContainer - changed to Customization Panel - ahsan - xit('Should render CustomizationPanel', () => { - expect( - target.find(customizationPanel) - ).toHaveLength(1); -}); -}); - -// testing for NavBar component -describe('Test NavBar component', () => { - const props = { - setTheme: jest.fn(), - isThemeLight: jest.fn(), - }; - const target = shallow ( - - - - ); - // testing for 4 generic buttons in NavBar - xit('Should render 2 buttons: "Clear Canvas", "Dark Mode"', () => { - expect(target.find('.navbarButton')).toHaveLength(2); - expect( - target - .find('.navbarButton') - .at(0) - .text(), - ).toEqual('Clear Canvas'); - expect( - target - .find('.navbarButton') - .at(1) - .text(), - ).toEqual('Dark Mode'); - - }); - - it('Should render "Login" button', () => { - const wrapper = shallow( ); - expect(wrapper).toHaveLength(1); - expect( - wrapper - .find('.navbarButton') - ).toHaveLength(1); -}); -}); - -describe('Test LeftContainer container', () => { - const target = shallow( - - - - ); - // test for the HTML panel (with all the html elements) on the left panel - it('Should render HTMLPanel', () => { - expect(target.find()).toBeDefined(); - }); -}); \ No newline at end of file diff --git a/__tests__/gql.projects.test.ts b/__tests__/gql.projects.test.ts index 3640394e5..212ea7c7f 100644 --- a/__tests__/gql.projects.test.ts +++ b/__tests__/gql.projects.test.ts @@ -1,14 +1,13 @@ -/** - * @jest-environment node - */ +// /** +// * @jest-environment node +// */ // const { Mongoose } = require('mongoose'); // const request = require('supertest'); // const http = require('http'); -// const app = require('../server/server.js'); +// const app = require('../server/server'); // const mock = require('../mockData'); - // tests user signup and login routes xdescribe('GraphQL tests', () => { let server; @@ -28,102 +27,117 @@ xdescribe('GraphQL tests', () => { }); // GraphQL Query - - describe('Testing GraphQL query', () => { - it('getAllProjects should return more than 1 project by default', () => request(server) - .post('/graphql') - .set('Content-Type', 'application/json') - .send({ - query: mock.GET_PROJECTS, - }) - .expect(200) - .then(res => expect(res.body.data.getAllProjects.length).toBeGreaterThanOrEqual(1))); - it('getAllProjects should return projects that matches the provided userId', () => request(server) - .post('/graphql') - .set('Content-Type', 'application/json') - .send({ - query: mock.GET_PROJECTS, - variables: { - userId: '604d21b2b61a1c95f2dc9105', - }, - }) - .expect(200) - .then(res => expect(res.body.data.getAllProjects[0].userId).toBe('604d21b2b61a1c95f2dc9105'))); + xdescribe('Testing GraphQL query', () => { + it('getAllProjects should return more than 1 project by default', () => + request(server) + .post('/graphql') + .set('Content-Type', 'application/json') + .send({ + query: mock.GET_PROJECTS + }) + .expect(200) + .then((res) => + expect(res.body.data.getAllProjects.length).toBeGreaterThanOrEqual(1) + )); + it('getAllProjects should return projects that matches the provided userId', () => + request(server) + .post('/graphql') + .set('Content-Type', 'application/json') + .send({ + query: mock.GET_PROJECTS, + variables: { + userId: '604d21b2b61a1c95f2dc9105' + } + }) + .expect(200) + .then((res) => + expect(res.body.data.getAllProjects[0].userId).toBe( + '604d21b2b61a1c95f2dc9105' + ) + )); }); // GraphQL Mutation - describe('Testing GraphQL mutation', () => { + xdescribe('Testing GraphQL mutation', () => { // Add likes - it('addLike should update the "likes" field of the project document', () => request(server) - .post('/graphql') - .set('Content-Type', 'application/json') - .send({ - query: mock.ADD_LIKE, - variables: { - projId: projectId, - likes: testNum, - }, - }) - .expect(200) - .then(res => expect(res.body.data.addLike.likes).toBe(testNum))); + it('addLike should update the "likes" field of the project document', () => + request(server) + .post('/graphql') + .set('Content-Type', 'application/json') + .send({ + query: mock.ADD_LIKE, + variables: { + projId: projectId, + likes: testNum + } + }) + .expect(200) + .then((res) => expect(res.body.data.addLike.likes).toBe(testNum))); // Publish project - it('Should set the "published" on the project document to TRUE', () => request(server) - .post('/graphql') - .set('Content-Type', 'application/json') - .send({ - query: mock.PUBLISH_PROJECT, - variables: { - projId: projectId, - published: true, - }, - }) - .expect(200) - .then(res => expect(res.body.data.publishProject.published).toBe(true))); - it('Should set the "published" on the project document to FALSE', () => request(server) - .post('/graphql') - .set('Content-Type', 'application/json') - .send({ - query: mock.PUBLISH_PROJECT, - variables: { - projId: projectId, - published: false, - }, - }) - .expect(200) - .then(res => expect(res.body.data.publishProject.published).toBe(false))); + it('Should set the "published" on the project document to TRUE', () => + request(server) + .post('/graphql') + .set('Content-Type', 'application/json') + .send({ + query: mock.PUBLISH_PROJECT, + variables: { + projId: projectId, + published: true + } + }) + .expect(200) + .then((res) => + expect(res.body.data.publishProject.published).toBe(true) + )); + it('Should set the "published" on the project document to FALSE', () => + request(server) + .post('/graphql') + .set('Content-Type', 'application/json') + .send({ + query: mock.PUBLISH_PROJECT, + variables: { + projId: projectId, + published: false + } + }) + .expect(200) + .then((res) => + expect(res.body.data.publishProject.published).toBe(false) + )); // Make copy - it('Should make a copy of an existing project and change the userId and userName', () => request(server) - .post('/graphql') - .set('Content-Type', 'application/json') - .send({ - query: mock.MAKE_COPY, - variables: { - projId: projectId, - userId: makeCopyUserIdTest, - username: makeCopyUsernameTest, - }, - }) - .expect(200) - .then((res) => { - expect(res.body.data.makeCopy.userId).toBe(makeCopyUserIdTest); - expect(res.body.data.makeCopy.username).toBe(makeCopyUsernameTest); - makeCopyProjId = res.body.data.makeCopy.id; - })); + it('Should make a copy of an existing project and change the userId and userName', () => + request(server) + .post('/graphql') + .set('Content-Type', 'application/json') + .send({ + query: mock.MAKE_COPY, + variables: { + projId: projectId, + userId: makeCopyUserIdTest, + username: makeCopyUsernameTest + } + }) + .expect(200) + .then((res) => { + expect(res.body.data.makeCopy.userId).toBe(makeCopyUserIdTest); + expect(res.body.data.makeCopy.username).toBe(makeCopyUsernameTest); + makeCopyProjId = res.body.data.makeCopy.id; + })); // Delete copy - it('Should make a copy of an existing project and change the userId and userName', () => request(server) - .post('/graphql') - .set('Content-Type', 'application/json') - .send({ - query: mock.DELETE_PROJECT, - variables: { - projId: makeCopyProjId, - }, - }) - .expect(200) - .then((res) => { - expect(res.body.data.deleteProject.id).toBe(makeCopyProjId); - })); + it('Should make a copy of an existing project and change the userId and userName', () => + request(server) + .post('/graphql') + .set('Content-Type', 'application/json') + .send({ + query: mock.DELETE_PROJECT, + variables: { + projId: makeCopyProjId + } + }) + .expect(200) + .then((res) => { + expect(res.body.data.deleteProject.id).toBe(makeCopyProjId); + })); }); - }); diff --git a/__tests__/helper.test.tsx b/__tests__/helper.test.tsx new file mode 100644 index 000000000..14ce14b3b --- /dev/null +++ b/__tests__/helper.test.tsx @@ -0,0 +1,7 @@ +import randomPassword from '../app/src/helperFunctions/randomPassword'; + +describe('Random Password', () => { + test('should generate password with 18 characters', () => { + expect(randomPassword()).toHaveLength(18); + }); +}); diff --git a/__tests__/playwright/example.spec.ts b/__tests__/playwright/example.spec.ts new file mode 100644 index 000000000..c952bc90a --- /dev/null +++ b/__tests__/playwright/example.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@playwright/test'; + +test('has title', async ({ page }) => { + await page.goto('https://app.reactype.dev/#/'); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle('ReacType'); +}); + +test('get started link', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Click the get started link. + await page.getByRole('link', { name: 'Get started' }).click(); + + // Expects the URL to contain intro. + await expect(page).toHaveURL(/.*intro/); +}); diff --git a/__tests__/projects.test.ts b/__tests__/projects.test.ts index 40ea5bc3e..7a68d4347 100644 --- a/__tests__/projects.test.ts +++ b/__tests__/projects.test.ts @@ -2,16 +2,16 @@ * @jest-environment node */ -// const { Mongoose } = require('mongoose'); -// const request = require('supertest'); +const { Mongoose } = require('mongoose'); +const request = require('supertest'); // initializes the project to be sent to server/DB -const { projectToSave, state } = require('../mockData'); -// const app = require('../server/server.js'); -// const http = require('http'); - +import mockData from '../mockData' +import app from ('../server/server'); +const http = require('http') +const {state, projectToSave } = mockData // save and get projects endpoint testing -xdescribe('Project endpoints tests', () => { +xdescribe('Project endpoints tests', () => { let server; beforeAll((done) => { server = http.createServer(app); @@ -22,7 +22,7 @@ xdescribe('Project endpoints tests', () => { server.close(done); }); // test saveProject endpoint - describe('/saveProject', () => { + xdescribe('/saveProject', () => { describe('/POST', () => { it('responds with a status of 200 and json object equal to project sent', () => { return request(server) @@ -36,7 +36,7 @@ xdescribe('Project endpoints tests', () => { }); }); // test getProjects endpoint - describe('/getProjects', () => { + xdescribe('/getProjects', () => { describe('POST', () => { it('responds with status of 200 and json object equal to an array of user projects', () => { return request(server) @@ -53,7 +53,7 @@ xdescribe('Project endpoints tests', () => { }); }); // test deleteProject endpoint - describe('/deleteProject', () => { + xdescribe('/deleteProject', () => { describe('DELETE', () => { const { name, userId } = projectToSave; it('responds with status of 200 and json object equal to deleted project', () => { diff --git a/__tests__/signIn.test.tsx b/__tests__/signIn.test.tsx new file mode 100644 index 000000000..755883e0e --- /dev/null +++ b/__tests__/signIn.test.tsx @@ -0,0 +1,51 @@ +import SignIn from '../app/src/components/login/SignIn'; +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import store from '../app/src/redux/store'; +import { BrowserRouter } from 'react-router-dom'; +import '@testing-library/jest-dom'; + +function TestSignIn() { + return ( + + + + + + ); +} + +describe('sign in page', () => { + test('should render a login input', () => { + render(); + expect(screen.getByTestId('username-input')).toBeInTheDocument(); + }); + test('should render a password field', () => { + render(); + expect(screen.getByTestId('password-input')).toBeInTheDocument(); + }); + test('should render 4 login buttons and dark mode button', () => { + render(); + expect(screen.getAllByRole('button')).toHaveLength(5); + }); + test('should invalidate empty username field', () => { + render(); + fireEvent.click(screen.getAllByRole('button')[1]); + waitFor(() => { + expect(screen.getByText('No Username Input')).toBeInTheDocument(); + }); + }); + test('should invalidate empty password field', () => { + render(); + fireEvent.change(screen.getByRole('textbox'), { + target: { + value: 'username' + } + }); + fireEvent.click(screen.getAllByRole('button')[1]); + waitFor(() => { + expect(screen.getByText('No Password Input')).toBeInTheDocument(); + }); + }); +}); diff --git a/__tests__/spec.ts b/__tests__/spec.ts index cec93216e..fe98fd3b4 100644 --- a/__tests__/spec.ts +++ b/__tests__/spec.ts @@ -1,32 +1,32 @@ import 'regenerator-runtime/runtime'; // if there is an error with moduleNameMapper, npm -S install regenerator-runtime //const { Application } = require('spectron'); -const electronPath = require('electron'); -const path = require('path'); +// const electronPath = require('electron'); +// const path = require('path'); -let app; +// let app; -// beforeAll(() => { -// // create a new app to test with setTimeout to be 15000 because the app takes a few seconds to spin up -// app = new Application({ -// path: electronPath, -// chromeDriverArgs: ['--disable-extensions'], -// args: [path.join(__dirname, '../app/electron/main.js')] // this is the path from this test file to main.js inside electron folder -// }); -// return app.start(); -// }, 15000); +// // beforeAll(() => { +// // // create a new app to test with setTimeout to be 15000 because the app takes a few seconds to spin up +// // app = new Application({ +// // path: electronPath, +// // chromeDriverArgs: ['--disable-extensions'], +// // args: [path.join(__dirname, '../app/electron/main.js')] // this is the path from this test file to main.js inside electron folder +// // }); +// // return app.start(); +// // }, 15000); -// getWindowsCount() will return 2 instead of 1 in dev mode (one for the actual app, one in the browser at localhost:8080 in dev mode) -xtest('Displays App window', async () => { - const windowCount = await app.client.getWindowCount(); - // expect(windowCount).toBe(1); // this returns true/passed if in production mode, change mode in script "test" to 'production' instead of 'test' - expect(windowCount).toBe(2); // 'dev' or 'test' mode results in 2 windows (one for the app and one for the browser) -}); - -/* we want to test other functionalities of app.client such as text, title, etc. but even the examples from the official spectron website -or github repo did not yield the same outcomes as demonstrated. So we stopped testing Electron app here */ -// afterAll(() => { -// if (app && app.isRunning()) { -// return app.stop(); -// } +// // getWindowsCount() will return 2 instead of 1 in dev mode (one for the actual app, one in the browser at localhost:8080 in dev mode) +// xtest('Displays App window', async () => { +// const windowCount = await app.client.getWindowCount(); +// // expect(windowCount).toBe(1); // this returns true/passed if in production mode, change mode in script "test" to 'production' instead of 'test' +// expect(windowCount).toBe(2); // 'dev' or 'test' mode results in 2 windows (one for the app and one for the browser) // }); + +// /* we want to test other functionalities of app.client such as text, title, etc. but even the examples from the official spectron website +// or github repo did not yield the same outcomes as demonstrated. So we stopped testing Electron app here */ +// // afterAll(() => { +// // if (app && app.isRunning()) { +// // return app.stop(); +// // } +// // }); diff --git a/__tests__/stateManagementReducer.test.js b/__tests__/stateManagementReducer.test.js index 71278a872..3b6d5e755 100644 --- a/__tests__/stateManagementReducer.test.js +++ b/__tests__/stateManagementReducer.test.js @@ -1,82 +1,73 @@ -import reducer from '../app/src/reducers/componentReducer'; -import initialState from '../app/src/context/initialState'; +import reducer from '../app/src/redux/reducers/slice/appStateSlice'; +import { initialState } from '../app/src/redux/reducers/slice/appStateSlice'; -describe('Testing componentReducer functionality for stateManagement tab', () => { - - const findComponent = (components, componentId) => { - return components.find(elem => elem.id === componentId); - }; - - let state = initialState; - - // setting up initial state - state.components = [ - { - "id": 1, - "name": "App", - "style": {}, - "code": "import React, { useState, useEffect, useContext} from 'react';\n\n\n\nimport C1 from './C1'\nconst App = (props) => {\n\n\n const [appState, setAppState] = useState(1);\n\n return(\n <>\n\n \n );\n}\n\nexport default App\n", - "children": [ - { - "type": "HTML Element", - "typeId": 1000, - "name": "separator", - "childId": 1000, - "style": {"border": "none"}, - "attributes": {}, - "children": [] - }, - { - "type": "Component", - "typeId": 2, - "name": "C1", - "childId": 1, - "style": {}, - "attributes": {}, - "children": [], - "stateProps": [], - "passedInProps": [ - ] - } - ], - "isPage": true, - "past": [ - [] - ], - "future": [], - "stateProps": [ - ], - "useStateCodes": [ - "const [appState, setAppState] = useState(1)" - ] - }, - { - "id": 2, - "name": "C1", - "nextChildId": 1, - "style": {}, - "attributes": {}, - "code": "import React, { useState, useEffect, useContext} from 'react';\n\n\n\n\nconst C1 = (props) => {\n\n\n\n return(\n <>\n\n \n );\n}\n\nexport default C1\n", - "children": [], - "isPage": false, - "past": [], - "future": [], - "stateProps": [], - "useStateCodes": [], - "passedInProps": [ - ] - } - ]; +//initializing copy of initial state to be used for test suite +let state = JSON.parse(JSON.stringify(initialState)); +state.components = [ + { + id: 1, + name: 'App', + style: {}, + code: "import React, { useState, useEffect, useContext} from 'react';\n\n\n\nimport C1 from './C1'\nconst App = (props) => {\n\n\n const [appState, setAppState] = useState(1);\n\n return(\n <>\n\n \n );\n}\n\nexport default App\n", + children: [ + { + type: 'HTML Element', + typeId: 1000, + name: 'separator', + childId: 1000, + style: { border: 'none' }, + attributes: {}, + children: [] + }, + { + type: 'Component', + typeId: 2, + name: 'C1', + childId: 1, + style: {}, + attributes: {}, + children: [], + stateProps: [], + passedInProps: [] + } + ], + isPage: true, + past: [[]], + future: [], + stateProps: [], + useStateCodes: [ + 'const [appState, setAppState] = useState(1)' + ] + }, + { + id: 2, + name: 'C1', + nextChildId: 1, + style: {}, + attributes: {}, + code: "import React, { useState, useEffect, useContext} from 'react';\n\n\n\n\nconst C1 = (props) => {\n\n\n\n return(\n <>\n\n \n );\n}\n\nexport default C1\n", + children: [], + isPage: false, + past: [], + future: [], + stateProps: [], + useStateCodes: [], + passedInProps: [] + } +]; - // TEST 'ADD STATE' - describe('ADD STATE test', () => { +const findComponent = (components, componentId) => { + return components.find((elem) => elem.id === componentId); +}; +describe('stateManagementReducer test', () => { + // TEST 'ADD STATE' + describe('addState', () => { // setting canvas focus to root component (App) - state.canvasFocus.componentId = 1; - + // state.canvasFocus.componentId = 1; // action dispatched to be tested - const action = { - type: 'ADD STATE', + const action1 = { + type: 'appState/addState', payload: { newState: { id: 'App-testAppState', @@ -89,51 +80,87 @@ describe('Testing componentReducer functionality for stateManagement tab', () => key: 'setTestAppState', type: 'func', value: '' + }, + contextParam: { + allContext: [] } - }, + } }; - + // setting test state - state = reducer(state, action); - const currComponent = findComponent(state.components, 1); - - it('should add state and its function modifier to the stateProps array of the current component', () => { + state = reducer(state, action1); + let currComponent = findComponent(state.components, 1); + + it('should add state and its setter function to the stateProps array of the current component', () => { expect(currComponent.stateProps.length).toEqual(2); }); - it(`state id should be 'App-testAppState'` , () => { + it(`state id should be 'App-testAppState'`, () => { expect(currComponent.stateProps[0].id).toEqual('App-testAppState'); }); - it(`state key should be 'testAppState'` , () => { + it(`state key should be 'testAppState'`, () => { expect(currComponent.stateProps[0].key).toEqual('testAppState'); }); - it(`state value should be 1` , () => { + it(`state value should be 1`, () => { expect(currComponent.stateProps[0].value).toEqual(1); }); - it(`state key should be number` , () => { + it(`state value type should be 'number'`, () => { expect(currComponent.stateProps[0].type).toEqual('number'); }); - it(`function state id should be 'App-setTestAppState'` , () => { + it(`function state id should be 'App-setTestAppState'`, () => { expect(currComponent.stateProps[1].id).toEqual('App-setTestAppState'); }); - it(`function state key should be 'setTestAppState'` , () => { + it(`function state key should be 'setTestAppState'`, () => { expect(currComponent.stateProps[1].key).toEqual('setTestAppState'); }); - it(`function state value should be blank` , () => { + it(`function state value should be blank`, () => { expect(currComponent.stateProps[1].value).toEqual(''); }); - it(`function state key should be func` , () => { + it(`function state key should be func`, () => { expect(currComponent.stateProps[1].type).toEqual('func'); }); + + const action2 = { + type: 'appState/addState', + payload: { + newState: { + id: 'App-testAppState2', + key: 'isLoggedIn', + type: 'boolean', + value: 'false' + }, + setNewState: { + id: 'App-setTestAppState2', + key: 'setIsLoggedIn', + type: 'func', + value: '' + }, + contextParam: { + allContext: [] + } + } + }; + + state = reducer(state, action2); + + describe('should handle value with type of boolean', () => { + it(`state key type should be boolean`, () => { + expect(state.components[0].stateProps[2].type).toEqual('boolean'); + }); + it(`state value should be false`, () => { + expect(state.components[0].stateProps[2].value).toEqual('false'); + }); + }); }); - + // TEST 'ADD PASSEDINPROPS' - describe('ADD PASSEDINPROPS test', () => { + describe('addPassedInProps', () => { + state = JSON.parse(JSON.stringify(state)); // setting canvas focus to the child component state.canvasFocus.componentId = 2; // action dispatched to be tested const action = { - type: 'ADD PASSEDINPROPS', + type: 'appState/addPassedInProps', payload: { passedInProps: { id: 'App-testAppState', @@ -141,14 +168,17 @@ describe('Testing componentReducer functionality for stateManagement tab', () => type: 'number', value: 1 }, - }, + contextParam: { + allContext: [] + } + } }; - + // setting test state state = reducer(state, action); const currComponent = findComponent(state.components, 2); const parentComponent = findComponent(state.components, 1); - + it(`current component should have a state id: 'App-testAppState' `, () => { expect(currComponent.passedInProps[0].id).toEqual('App-testAppState'); }); @@ -161,64 +191,78 @@ describe('Testing componentReducer functionality for stateManagement tab', () => it(`current component should have a state value type: 'number'`, () => { expect(currComponent.passedInProps[0].type).toEqual('number'); }); - //check parent children array to make sure it is being added here as well it(`parent component 'passedInProps' array length should be 1`, () => { expect(currComponent.passedInProps.length).toEqual(1); }); it(`parent component should have a state id: 'App-testAppState' `, () => { - expect(parentComponent.children[1].passedInProps[0].id).toEqual('App-testAppState'); + expect(parentComponent.children[1].passedInProps[0].id).toEqual( + 'App-testAppState' + ); }); it(`parent component should have a state key: 'testAppState' `, () => { - expect(parentComponent.children[1].passedInProps[0].key).toEqual('testAppState'); + expect(parentComponent.children[1].passedInProps[0].key).toEqual( + 'testAppState' + ); }); it(`parent component should have a state value equal to 1`, () => { expect(parentComponent.children[1].passedInProps[0].value).toEqual(1); }); it(`parent component should have a state value type: 'number'`, () => { - expect(parentComponent.children[1].passedInProps[0].type).toEqual('number'); + expect(parentComponent.children[1].passedInProps[0].type).toEqual( + 'number' + ); }); }); - + // TEST 'DELETE PASSEDINPROPS' - describe('DELETE PASSEDINPROPS test', () => { + describe('deletePassedInProps', () => { it('should delete the state passed down from parent component in the child component', () => { // setting canvas focus to the child component + state = JSON.parse(JSON.stringify(state)); state.canvasFocus.componentId = 2; - + // action dispatched to be tested const action = { - type: 'DELETE PASSEDINPROPS', - payload: { rowId: 'App-testAppState' }, + type: 'appState/deletePassedInProps', + payload: { + rowId: 'App-testAppState', + contextParam: { + allContext: [] + } + } }; - + // setting test state state = reducer(state, action); const parentComponent = findComponent(state.components, 1); const currComponent = findComponent(state.components, 2); - + expect(currComponent.passedInProps.length).toEqual(0); expect(parentComponent.children[1].passedInProps.length).toEqual(0); // need to fix reducer }); }); // TEST 'DELETE STATE' - describe('DELETE STATE test', () => { + describe('deleteState', () => { it('should delete all instances of state from stateProps and passedInProps', () => { - // setting canvas focus to root component + state = JSON.parse(JSON.stringify(state)); state.canvasFocus.componentId = 1; // action dispatched to be tested const action = { - "type": "DELETE STATE", - "payload": { - "stateProps": [], - "rowId": "App-appState", - "otherId": "App-setAppState" + type: 'appState/deleteState', + payload: { + stateProps: [], + rowId: 'App-appState', + otherId: 'App-setAppState', + contextParam: { + allContext: [] + } } }; - + // setting intial test state let parentComponent = findComponent(state.components, 1); parentComponent.children[1].passedInProps = [ @@ -235,7 +279,7 @@ describe('Testing componentReducer functionality for stateManagement tab', () => value: '' } ]; - + let childComponent = findComponent(state.components, 2); childComponent.passedInProps = [ { @@ -251,15 +295,15 @@ describe('Testing componentReducer functionality for stateManagement tab', () => value: '' } ]; - + // updating components after state updates state = reducer(state, action); parentComponent = findComponent(state.components, 1); childComponent = findComponent(state.components, 2); expect(childComponent.passedInProps.length).toEqual(0); - expect(parentComponent.stateProps.length).toEqual(0); - expect(parentComponent.children[1].passedInProps.length).toEqual(0); + expect(parentComponent.stateProps.length).toEqual(0); + expect(parentComponent.children[1].passedInProps.length).toEqual(0); }); }); }); diff --git a/__tests__/tree.test.tsx b/__tests__/tree.test.tsx index 1b1ad9313..395e63da8 100644 --- a/__tests__/tree.test.tsx +++ b/__tests__/tree.test.tsx @@ -1,16 +1,14 @@ import TreeChart from '../app/src/tree/TreeChart'; -import React, { useReducer } from 'react'; +import React from 'react'; import '@testing-library/jest-dom'; -import { - render, screen, -} from '@testing-library/react'; -import StateContext from '../app/src/context/context'; -import initialState from '../app/src/context/initialState'; -import reducer from '../app/src/reducers/componentReducer'; +import { render, screen } from '@testing-library/react'; +import { initialState } from '../app/src/redux/reducers/slice/appStateSlice'; +import { Provider } from 'react-redux'; +import store from '../app/src/redux/store'; import 'd3'; -// tester populates the components array used for this testing suite -const tester = [ +let state = JSON.parse(JSON.stringify(initialState)); +state.components = [ { id: 1, name: 'index', @@ -49,13 +47,13 @@ const tester = [ name: 'A', style: {}, type: 'Component', - typeId: 2, - }, + typeId: 2 + } ], name: 'div', style: {}, type: 'HTML Element', - typeId: 11, + typeId: 11 }, { childId: 3, @@ -66,16 +64,16 @@ const tester = [ name: 'B', style: {}, type: 'Component', - typeId: 3, - }, + typeId: 3 + } ], name: 'div', style: {}, type: 'HTML Element', - typeId: 11, - }, + typeId: 11 + } ], - isPage: true, + isPage: true }, { id: 2, @@ -84,7 +82,7 @@ const tester = [ style: {}, code: '', children: [], - isPage: false, + isPage: false }, { id: 3, @@ -93,27 +91,23 @@ const tester = [ style: {}, code: '', children: [], - isPage: false, - }, + isPage: false + } ]; // renders a tree of the components in tester -function Test() { - const [state, dispatch] = useReducer(reducer, initialState); - state.components = tester; - return ( - - - - ); -} - -test('Test the tree functionality', () => { - render(); - // elements that are not separators should appear in the tree - expect(screen.getByText('index')).toBeInTheDocument(); - expect(screen.getByText('A')).toBeInTheDocument(); - expect(screen.getByText('B')).toBeInTheDocument(); - // tree should not include separators - expect(screen.queryByText('separator')).toBe(null); +describe('Component Tree Render Test', () => { + test('should render full component tree based on state', () => { + render( + + + + ); + // elements that are not separators should appear in the tree + expect(screen.getByText('index')).toBeInTheDocument(); + expect(screen.getByText('A')).toBeInTheDocument(); + expect(screen.getByText('B')).toBeInTheDocument(); + // tree should not include separators + expect(screen.queryByText('separator')).toBe(null); + }); }); diff --git a/__tests__/userAuth.test.ts b/__tests__/userAuth.test.ts index eb32abbbc..23e4534a1 100644 --- a/__tests__/userAuth.test.ts +++ b/__tests__/userAuth.test.ts @@ -1,80 +1,168 @@ /** * @jest-environment node */ +import request from 'supertest'; +import app from '../server/server'; +import mockObj from '../mockData'; +const user = mockObj.user; +import mongoose from 'mongoose'; +const URI = process.env.MONGO_DB; + +beforeAll(() => { + mongoose + .connect(URI, { useNewUrlParser: true }, { useUnifiedTopology: true }) + .then(() => console.log('connected to test database')); +}); -import { sessionIsCreated, newUserIsCreated } from '../app/src/helperFunctions/auth'; - - - -// const { Mongoose } = require('mongoose'); - -// const http = require('http'); -// const app = require('../server/server.js'); - -let server; - -// tests auth.ts helper function and associated server routes - -xdescribe('Login Tests', () => { - jest.setTimeout(10000); - let username; - let password; - let isFbOauth; // whether OAuth is used - - - beforeAll((done) => { - server = http.createServer(app); - server.listen(done); - }); - - afterAll((done) => { - Mongoose.disconnect(); - server.close(done); +afterAll(async () => { + await mongoose.connection.close(); +}); +//for creating unqiue login credentials +const num = Math.floor(Math.random() * 1000); + +describe('User authentication tests', () => { + //test connection to server + describe('initial connection test', () => { + it('should connect to the server', async () => { + const response = await request(app).get('/test'); + expect(response.text).toEqual('test request is working'); + }); }); - - // Called under SignIn.tsx - - describe('sessionIsCreated', () => { - it('returns the message \'No Username Input\' when no username is entered', async () => { - username = ''; - password = 'Reactype123!@#'; - isFbOauth = false; - const result = await sessionIsCreated(username, password, isFbOauth).then((loginStatus) => loginStatus); - expect(result).toEqual('No Username Input'); + xdescribe('POST', () => { + it('responds with status 200 and json object on valid new user signup', () => { + return request(app) + .post('/signup') + .set('Content-Type', 'application/json') + .send({ + username: `supertest${num}`, + email: `test${num}@test.com`, + password: `${num}` + }) + .expect(200) + .then((res) => expect(typeof res.body).toBe('object')); }); - it('returns the message \'No Password Input\' when no password is entered', async () => { - username = 'reactype123'; - password = ''; - isFbOauth = false; - const result = await sessionIsCreated(username, password, isFbOauth).then((loginStatus) => loginStatus); - expect(result).toEqual('No Password Input'); + it('responds with status 400 and json string on invalid new user signup', () => { + return request(app) + .post('/signup') + .send(user) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(400) + .then((res) => expect(typeof res.body).toBe('string')); }); - - it('returns the message \'Invalid Username\' when username does not exist', async () => { - username = 'l!b'; //breaks the 4 character minimum and no special characters restriction - password = 'test'; - isFbOauth = false; - const result = await sessionIsCreated(username, password, isFbOauth).then((loginStatus) => loginStatus); - expect(result).toEqual('Invalid Username'); + }); +}); +describe('/login', () => { + // tests whether existing login information permits user to log in + xdescribe('POST', () => { + it('responds with status 200 and json object on verified user login', () => { + return request(app) + .post('/login') + .set('Accept', 'application/json') + .send(user) + .expect(200) + .expect('Content-Type', /json/) + .then((res) => expect(res.body.sessionId).toEqual(user.userId)); }); - - it('returns the message \'Incorrect Password\' when password does not match', async () => { - username = 'reactyp3test'; - password = 'incorrect'; - isFbOauth = false; - const result = await sessionIsCreated(username, password, isFbOauth).then((loginStatus) => loginStatus); - expect(result).toEqual('Incorrect Password'); + // if invalid username/password, should respond with status 400 + it('responds with status 400 and json string on invalid user login', () => { + return request(app) + .post('/login') + .send({ username: 'wrongusername', password: 'wrongpassword' }) + .expect(400) + .expect('Content-Type', /json/) + .then((res) => expect(typeof res.body).toBe('string')); }); - // note that the username and password in this test are kept in the heroku database - // DO NOT CHANGE unless you have access to the heroku database - it('returns the message \'Success\' when the user passes all auth checks', async () => { - username = 'testing'; - password = 'codesmith1!'; - isFbOauth = false; - const result = await sessionIsCreated(username, password, isFbOauth).then((loginStatus) => loginStatus); - expect(result).toEqual('Success'); + it('responds with status 400 and json string on invalid new user signup', () => { + return request(app) + .post('/signup') + .send(user) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(400) + .then((res) => expect(typeof res.body).toBe('string')); }); }); }); + +describe('sessionIsCreated', () => { + it("returns the message 'No Username Input' when no username is entered", () => { + return request(app) + .post('/login') + .send({ + username: '', + password: 'Reactype123!@#', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"No Username Input"')); + }); + + it("returns the message 'No Password Input' when no password is entered", () => { + return request(app) + .post('/login') + .send({ + username: 'reactype123', + password: '', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"No Password Input"')); + }); + + it("returns the message 'Invalid Username' when username does not exist", () => { + return request(app) + .post('/login') + .send({ + username: 'l!b', + password: 'test', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"Invalid Username"')); + }); +}); + +it("returns the message 'Incorrect Password' when password does not match", () => { + return request(app) + .post('/login') + .send({ + username: 'test', + password: 'test', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"Incorrect Password"')); +}); +// note that the username and password in this test are kept in the heroku database +// DO NOT CHANGE unless you have access to the heroku database +it("returns the message 'Success' when the user passes all auth checks", () => { + return request(app) + .post('/login') + .send({ + username: 'test', + password: 'password1!', + isFbOauth: false + }) + .then((res) => expect(res.body).toHaveProperty('sessionId')); +}); + +// // // OAuth tests (currently inoperative) + +// // xdescribe('Github oauth tests', () => { +// // describe('/github/callback?code=', () => { +// // describe('GET', () => { +// // it('responds with status 400 and error message if no code received', () => { +// // return request(server) +// // .get('/github/callback?code=') +// // .expect(400) +// // .then((res) => { +// // return expect(res.text).toEqual( +// // '"Undefined or no code received from github.com"' +// // ); +// // }); +// // }); +// // it('responds with status 400 if invalid code received', () => { +// // return request(server).get('/github/callback?code=123456').expect(400); +// // }); +// // }); +// // }); +// // }); diff --git a/__tests__/users.test.ts b/__tests__/users.test.ts deleted file mode 100644 index 045238818..000000000 --- a/__tests__/users.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * @jest-environment node - */ - -// const { Mongoose } = require('mongoose'); -// const request = require('supertest'); -// const http = require('http'); -// const app = require('../server/server.js'); - -// const browser = 'http://localhost:8080'; // for checking endpoints accessed with hash router - -// const { user } = require('../mockData'); - -// tests user signup and login routes -xdescribe('User authentication tests', () => { - let server; - - beforeAll((done)=> { - server = http.createServer(app); - server.listen(done); - }); - - afterAll((done)=> { - Mongoose.disconnect(); - server.close(done); - }); - - - const num = Math.floor(Math.random() * 1000); - - // tests whether signup page is returned on navigation to /#/signup endpoint - // note that /#/ is required in endpoint because it is accessed via hash router - - describe('/signup', () => { - describe('GET', () => { - it('respond with status 200 and load signup file', () => { - return request(browser) - .get('/#/signup') - .expect('Content-Type', /text\/html/) - .expect(200); - }); - }); - // tests whether new user can sign up - describe('POST', () => { - it('responds with status 200 and json object on valid new user signup', () => { - return request(server) - .post('/signup') - .send({ - username: `supertest${num}`, - email: `test${num}@test.com`, - password: `${num}`, - }) - .set('Content-Type', 'application/json') - .expect(200) - .then(res => expect(typeof res.body).toBe('object')); - }); - // if invalid signup input, should respond with status 400 - it('responds with status 400 and json string on invalid new user signup', () => { - return request(server) - .post('/signup') - .send(user) - .set('Accept', 'application/json') - .expect('Content-Type', /json/) - .expect(400) - .then(res => expect(typeof res.body).toBe('string')); - }); - }); - }); - // tests whether login page is returned on navigation to /#/login endpoint - - describe('/login', () => { - describe('GET', () => { - it('respond with status 200 and load login file', () => { - return request(browser) - .get('/#/login') - .expect('Content-Type', /text\/html/) - .expect(200); - }); - }); - // tests whether existing login information permits user to log in - describe('POST', () => { - it('responds with status 200 and json object on verified user login', () => { - return request(server) - .post('/login') - .set('Accept', 'application/json') - .send(user) - .expect(200) - .expect('Content-Type', /json/) - .then(res => expect(res.body.sessionId).toEqual(user.userId)); - }); - // if invalid username/password, should respond with status 400 - it('responds with status 400 and json string on invalid user login', () => { - return request(server) - .post('/login') - .send({ username: 'wrongusername', password: 'wrongpassword' }) - .expect(400) - .expect('Content-Type', /json/) - .then((res) => expect(typeof res.body).toBe('string')); - }); - }); - }); -}); - -// OAuth tests (currently inoperative) - -xdescribe('Github oauth tests', () => { - describe('/github/callback?code=', () => { - describe('GET', () => { - it('responds with status 400 and error message if no code received', () => { - return request(server) - .get('/github/callback?code=') - .expect(400) - .then((res) => { - return expect(res.text).toEqual('\"Undefined or no code received from github.com\"'); - }); - }); - it('responds with status 400 if invalid code received', () => { - return request(server) - .get('/github/callback?code=123456') - .expect(400) - }); - }); - }); -}); - diff --git a/app/.electron/main.js b/app/.electron/main.js index ef1805ed7..c4eac1c3b 100644 --- a/app/.electron/main.js +++ b/app/.electron/main.js @@ -5,7 +5,7 @@ const electron = require('electron'); @actions: codes for Github Oauth has been commented out because of lack of functionality. */ require('dotenv').config(); -const { DEV_PORT } = require('../../config'); +const { DEV_PORT } = require('../../config.js'); const path = require('path'); const { app, @@ -79,7 +79,7 @@ async function createWindow() { enableRemoteModule: true, // path of preload script. preload is how the renderer page will have access to electron functionality preload: path.join(__dirname, 'preload.js'), - nativeWindowOpen: true, + nativeWindowOpen: true } }); @@ -159,13 +159,16 @@ async function createWindow() { // we could use this over _all_ urls ses .fromPartition(partition) - .webRequest.onBeforeRequest({ urls: ['http://localhost./*'] }, listener => { - if (listener.url.indexOf('http://') >= 0) { - listener.callback({ - cancel: true - }); + .webRequest.onBeforeRequest( + { urls: ['http://localhost./*'] }, + (listener) => { + if (listener.url.indexOf('http://') >= 0) { + listener.callback({ + cancel: true + }); + } } - }); + ); } // Needs to be called before app is ready; @@ -336,7 +339,7 @@ app.on('remote-get-current-web-contents', (event, webContents) => { // When a user selects "Export project", a function (chooseAppDir loaded via preload.js) // is triggered that sends a "choose_app_dir" message to the main process // when the "choose_app_dir" message is received it triggers this event listener -ipcMain.on('choose_app_dir', event => { +ipcMain.on('choose_app_dir', (event) => { // dialog displays the native system's dialogue for selecting files // once a directory is chosen send a message back to the renderer with the path of the directory dialog @@ -344,11 +347,11 @@ ipcMain.on('choose_app_dir', event => { properties: ['openDirectory'], buttonLabel: 'Export' }) - .then(directory => { + .then((directory) => { if (!directory) return; event.sender.send('app_dir_selected', directory.filePaths[0]); }) - .catch(err => console.log('ERROR on "choose_app_dir" event: ', err)); + .catch((err) => console.log('ERROR on "choose_app_dir" event: ', err)); }); // define serverURL for cookie and auth purposes based on environment @@ -358,32 +361,32 @@ if (isDev) { } // // for github oauth login in production, since cookies are not accessible through document.cookie on local filesystem, we need electron to grab the cookie that is set from oauth, this listens for an set cookie event from the renderer process then sends back the cookie -ipcMain.on('set_cookie', event => { +ipcMain.on('set_cookie', (event) => { session.defaultSession.cookies .get({ url: serverUrl }) - .then(cookie => { + .then((cookie) => { // this if statement is necessary or the setInterval on main app will constantly run and will emit this event.reply, causing a memory leak // checking for a cookie inside array will only emit reply when a cookie exists if (cookie[0]) { event.reply('give_cookie', cookie); } }) - .catch(error => { + .catch((error) => { console.log('Error giving cookies in set_cookie:', error); }); }); // again for production, document.cookie is not accessible so we need this listener on main to delete the cookie on logout -ipcMain.on('delete_cookie', event => { +ipcMain.on('delete_cookie', (event) => { session.defaultSession.cookies .remove(serverUrl, 'ssid') // .then(removed => { // }) - .catch(err => console.log('Error deleting cookie:', err)); + .catch((err) => console.log('Error deleting cookie:', err)); }); // opens new window for github oauth when button on sign in page is clicked -ipcMain.on('github', event => { +ipcMain.on('github', (event) => { const githubURL = isDev ? `http://localhost:${DEV_PORT}/auth/github` : `https://reactype-caret.herokuapp.com/auth/github`; @@ -409,7 +412,7 @@ ipcMain.on('github', event => { github.loadURL(githubURL); github.show(); - const handleCallback = url => { + const handleCallback = (url) => { const raw_code = /code=([^&]\*)/.exec(url) || null; const code = raw_code && raw_code.length > 1 ? raw_code[1] : null; const error = /\?error=(.+)\$/.exec(url); @@ -463,13 +466,13 @@ ipcMain.on('github', event => { github.close(); win.webContents .executeJavaScript(`window.localStorage.setItem('ssid', '${ssid}')`) - .then(result => win.loadURL(`${redirectUrl}`)) - .catch(err => console.log(err)); + .then((result) => win.loadURL(`${redirectUrl}`)) + .catch((err) => console.log(err)); } }); }); -ipcMain.on('tutorial', event => { +ipcMain.on('tutorial', (event) => { // create new browser window object with size, title, security options const tutorial = new BrowserWindow({ width: 800, diff --git a/app/src/components/App.tsx b/app/src/components/App.tsx index a7d16e347..a46e188f6 100644 --- a/app/src/components/App.tsx +++ b/app/src/components/App.tsx @@ -8,11 +8,13 @@ import { saveProject } from '../helperFunctions/projectGetSaveDel'; import Cookies from 'js-cookie'; //redux toolkit addition import { useSelector, useDispatch } from 'react-redux'; -import { setInitialState, toggleLoggedIn, configToggle } from '../redux/reducers/slice/appStateSlice'; +import { setInitialState, toggleLoggedIn} from '../redux/reducers/slice/appStateSlice'; + +import { RootState } from '../redux/store'; // Intermediary component to wrap main App component with higher order provider components export const App = (): JSX.Element => { - const state = useSelector(store => store.appState); + const state = useSelector((store: RootState) => store.appState); const dispatch = useDispatch(); // checks if user is signed in as guest or actual user and changes loggedIn boolean accordingly useEffect(()=>{ diff --git a/app/src/components/ContextAPIManager/AssignTab/AssignContainer.tsx b/app/src/components/ContextAPIManager/AssignTab/AssignContainer.tsx index 2cca2dc74..e27f886fd 100644 --- a/app/src/components/ContextAPIManager/AssignTab/AssignContainer.tsx +++ b/app/src/components/ContextAPIManager/AssignTab/AssignContainer.tsx @@ -8,8 +8,9 @@ import ComponentTable from './components/ComponentTable'; import { Button } from '@mui/material'; import DoubleArrowIcon from '@mui/icons-material/DoubleArrow'; import { addComponentToContext } from '../../../redux/reducers/slice/contextReducer'; -import { useSelector, useDispatch, useStore } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; import { deleteElement } from '../../../redux/reducers/slice/appStateSlice'; +import { RootState } from '../../../redux/store'; const AssignContainer = () => { const dispatch = useDispatch(); @@ -18,31 +19,21 @@ const AssignContainer = () => { const [contextInput, setContextInput] = React.useState(null); const [componentInput, setComponentInput] = React.useState(null); const [componentTable, setComponentTable] = useState([]); - const { state, contextParam } = useSelector((store) => ({ + const { state, contextParam } = useSelector((store: RootState) => ({ state: store.appState, contextParam: store.contextSlice })); - - + //sets table data if it exists const renderTable = (targetContext) => { - if (targetContext === null || !targetContext.values) { - setTableState(defaultTableData); - } else { - setTableState(targetContext.values); - } + targetContext?.values && setTableState(targetContext.values); }; //construct data for table displaying component table const renderComponentTable = (targetComponent) => { //target Component is main - const listOfContexts = []; - if ( - !Array.isArray(state) && - targetComponent !== null && - targetComponent.name - ) { + if (!Array.isArray(state) && targetComponent?.name) { contextParam.allContext.forEach((context) => { if (context.components.includes(targetComponent.name)) { listOfContexts.push(context.name); diff --git a/app/src/components/ContextAPIManager/AssignTab/components/ComponentDropDrown.tsx b/app/src/components/ContextAPIManager/AssignTab/components/ComponentDropDrown.tsx index 938b6c1b8..f2267df02 100644 --- a/app/src/components/ContextAPIManager/AssignTab/components/ComponentDropDrown.tsx +++ b/app/src/components/ContextAPIManager/AssignTab/components/ComponentDropDrown.tsx @@ -1,26 +1,22 @@ -import React, { Fragment, useState, useEffect, useContext } from 'react'; +import React, { Fragment } from 'react'; import TextField from '@mui/material/TextField'; import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete'; import Box from '@mui/material/Box'; - import { useSelector } from 'react-redux'; -import { Store } from 'redux'; +import { RootState } from '../../../../redux/store'; const filter = createFilterOptions(); const ComponentDropDown = ({ - contextStore, renderComponentTable, componentInput, setComponentInput }) => { - const { allContext } = contextStore; - // const [componentList] = useContext(StateContext); - const {state, isDarkMode} = useSelector(store =>({ - state: store.appState, + const { state, isDarkMode } = useSelector((store: RootState) => ({ + state: store.appState, isDarkMode: store.darkMode.isDarkMode - } )) - const color = isDarkMode ? "white":"black" + })); + const color = isDarkMode ? 'white' : 'black'; const onChange = (event, newValue) => { if (typeof newValue === 'string') { setComponentInput({ @@ -44,7 +40,7 @@ const ComponentDropDown = ({ const filtered = filter(options, params); const { inputValue } = params; // Suggest the creation of a new contextInput - const isExisting = options.some(option => inputValue === option.name); + const isExisting = options.some((option) => inputValue === option.name); if (inputValue !== '' && !isExisting) { filtered.push({ inputValue, @@ -57,7 +53,7 @@ const ComponentDropDown = ({ return filtered; }; - const getOptionLabel = option => { + const getOptionLabel = (option) => { // Value selected with enter, right from the input if (typeof option === 'string') { return option; @@ -70,11 +66,15 @@ const ComponentDropDown = ({ return option.name; }; - const renderOption = (props, option) =>
  • {option.name}
  • ; + const renderOption = (props, option) => ( +
  • + {option.name} +
  • + ); return ( - + ( - + renderInput={(params) => ( + )} /> @@ -100,4 +105,4 @@ const ComponentDropDown = ({ ); }; -export default ComponentDropDown; \ No newline at end of file +export default ComponentDropDown; diff --git a/app/src/components/ContextAPIManager/AssignTab/components/ContextDropDown.tsx b/app/src/components/ContextAPIManager/AssignTab/components/ContextDropDown.tsx index 600a5a003..52644cc97 100644 --- a/app/src/components/ContextAPIManager/AssignTab/components/ContextDropDown.tsx +++ b/app/src/components/ContextAPIManager/AssignTab/components/ContextDropDown.tsx @@ -1,10 +1,9 @@ import React, { Fragment, useState, useEffect } from 'react'; import TextField from '@mui/material/TextField'; import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete'; -import Button from '@mui/material/Button'; import Box from '@mui/material/Box'; -import { Typography } from '@mui/material'; import { useSelector } from 'react-redux'; +import { RootState } from '../../../../redux/store'; const filter = createFilterOptions(); @@ -16,8 +15,10 @@ const ContextDropDown = ({ }) => { const { allContext } = contextStore; - const isDarkMode = useSelector(store => store.darkMode.isDarkMode) -const color = isDarkMode ? "white":"black" + const isDarkMode = useSelector( + (store: RootState) => store.darkMode.isDarkMode + ); + const color = isDarkMode ? 'white' : 'black'; const onChange = (event, newValue) => { if (typeof newValue === 'string') { setContextInput({ @@ -41,7 +42,7 @@ const color = isDarkMode ? "white":"black" const filtered = filter(options, params); const { inputValue } = params; // Suggest the creation of a new contextInput - const isExisting = options.some(option => inputValue === option.name); + const isExisting = options.some((option) => inputValue === option.name); if (inputValue !== '' && !isExisting) { filtered.push({ inputValue, @@ -54,7 +55,7 @@ const color = isDarkMode ? "white":"black" return filtered; }; - const getOptionLabel = option => { + const getOptionLabel = (option) => { // Value selected with enter, right from the input if (typeof option === 'string') { return option; @@ -67,11 +68,15 @@ const color = isDarkMode ? "white":"black" return option.name; }; - const renderOption = (props, option) =>
  • {option.name}
  • ; + const renderOption = (props, option) => ( +
  • + {option.name} +
  • + ); return ( - + ( - + renderInput={(params) => ( + )} /> @@ -99,4 +107,4 @@ const color = isDarkMode ? "white":"black" ); }; -export default ContextDropDown; \ No newline at end of file +export default ContextDropDown; diff --git a/app/src/components/ContextAPIManager/AssignTab/components/ContextTable.tsx b/app/src/components/ContextAPIManager/AssignTab/components/ContextTable.tsx index f3734bc34..89ec16fc2 100644 --- a/app/src/components/ContextAPIManager/AssignTab/components/ContextTable.tsx +++ b/app/src/components/ContextAPIManager/AssignTab/components/ContextTable.tsx @@ -45,11 +45,13 @@ const rows = [ createData('Cupcake', 305, 3.7, 67, 4.3), createData('Gingerbread', 356, 16.0, 49, 3.9) ]; -{/*
    */} +{ + /*
    */ +} export default function ContextTable() { return ( - +
    Context @@ -57,7 +59,7 @@ export default function ContextTable() { - {rows.map(row => ( + {rows.map((row) => ( {row.name} @@ -69,4 +71,4 @@ export default function ContextTable() {
    ); -} \ No newline at end of file +} diff --git a/app/src/components/ContextAPIManager/ContextManager.tsx b/app/src/components/ContextAPIManager/ContextManager.tsx index a37a24e59..81c1395e9 100644 --- a/app/src/components/ContextAPIManager/ContextManager.tsx +++ b/app/src/components/ContextAPIManager/ContextManager.tsx @@ -1,6 +1,4 @@ - - -import React, { useContext } from 'react'; + import React, { useContext } from 'react'; import { makeStyles } from '@mui/styles'; import Box from '@mui/material/Box'; import Tab from '@mui/material/Tab'; @@ -12,6 +10,7 @@ import CreateContainer from './CreateTab/CreateContainer'; import AssignContainer from './AssignTab/AssignContainer'; import DisplayContainer from './DisplayTab/DisplayContainer'; import { useSelector } from 'react-redux' +import { RootState } from '../../redux/store'; const useStyles = makeStyles({ @@ -22,7 +21,7 @@ const useStyles = makeStyles({ }); const ContextManager = (props): JSX.Element => { - const { isDarkMode, style } = useSelector((store) => ({ + const { isDarkMode, style } = useSelector((store:RootState) => ({ isDarkMode: store.darkMode.isDarkMode, style: store.styleSlice })); @@ -40,10 +39,10 @@ const ContextManager = (props): JSX.Element => {
    - + - + diff --git a/app/src/components/ContextAPIManager/CreateTab/CreateContainer.tsx b/app/src/components/ContextAPIManager/CreateTab/CreateContainer.tsx index 8c8c8327d..5d182e4ea 100644 --- a/app/src/components/ContextAPIManager/CreateTab/CreateContainer.tsx +++ b/app/src/components/ContextAPIManager/CreateTab/CreateContainer.tsx @@ -1,87 +1,88 @@ -import React, { useEffect, useState, useContext } from 'react'; -import { useStore } from 'react-redux'; +import React from 'react'; import Divider from '@mui/material/Divider'; import Grid from '@mui/material/Grid'; import DataTable from './components/DataTable'; import AddDataForm from './components/AddDataForm'; import AddContextForm from './components/AddContextForm'; -// import * as actions from '../../../redux/actions/actions'; import { Typography } from '@mui/material'; -import StateContext from '../../../context/context'; -import { addContext, deleteContext, addContextValues } from '../../../redux/reducers/slice/contextReducer'; +import { + addContext, + deleteContext, + addContextValues +} from '../../../redux/reducers/slice/contextReducer'; import { useSelector, useDispatch } from 'react-redux'; -import { deleteElement } from '../../../redux/reducers/slice/appStateSlice'; +import { RootState } from '../../../redux/store'; const CreateContainer = () => { - const defaultTableData = [{ key: 'Enter Key', value: 'Enter value' }]; - const state = useSelector(store => store.contextSlice); - - const store = useStore(); - // const [state, setState] = useState([]); - const [tableState, setTableState] = React.useState(defaultTableData); + const state = useSelector((store: RootState) => store.contextSlice); const [contextInput, setContextInput] = React.useState(null); - // const [stateContext, dispatchContext] = useContext(StateContext); + const [currentContext, setCurrentContext] = React.useState(null); + const [errorMsg, setErrorMsg] = React.useState(''); + const [errorStatus, setErrorStatus] = React.useState(false); + const currentKeyValues = state.allContext.find( + (obj) => obj.name === currentContext + )?.values || [{ key: 'Enter Key', value: 'Enter value' }]; const dispatch = useDispatch(); - //pulling data from redux store - // useEffect(() => { - - // setState(allContext) - // // setState(store.getState().contextSlice); - - // }, [allContext]); - - - //update data store when user adds a new context const handleClickSelectContext = () => { - //prevent user from adding duplicate context - for (let i = 0; i < state.allContext.length; i += 1) { - if (state.allContext[i].name === contextInput.name) { - return; - } + let letters = /[a-zA-Z]/; + let error; + //checking for input error / setting error type + if (!contextInput || contextInput.trim() === '') { + error = 'empty'; + } else if (!contextInput.charAt(0).match(letters)) { + error = 'letters'; + } else if (!contextInput.match(/^[0-9a-zA-Z]+$/)) { + error = 'symbolsDetected'; + } else if ( + state.allContext.some( + (context) => context.name.toLowerCase() === contextInput.toLowerCase() + ) + ) { + error = 'dupe'; + } + + if (error !== undefined) { + triggerError(error); + return; } + + dispatch(addContext({ name: contextInput })); setContextInput(''); - dispatch(addContext(contextInput)); + }; - // setState(allContext); + const triggerError = (type: String) => { + setErrorStatus(true); + switch (type) { + case 'empty': + setErrorMsg('Context name cannot be blank.'); + break; + case 'dupe': + setErrorMsg('Context name already exists.'); + break; + case 'letters': + setErrorMsg('Context name must start with a letter.'); + break; + case 'symbolsDetected': + setErrorMsg('Context name must not contain symbols.'); + break; + } }; //update data store when user add new key-value pair to context - const handleClickInputData = ({ name }, { inputKey, inputValue }) => { - dispatch( - addContextValues({ name, inputKey, inputValue }) - ); - // setState(allContext); + const handleClickInputData = (name, { inputKey, inputValue }) => { + dispatch(addContextValues({ name, inputKey, inputValue })); }; //update data store when user deletes context const handleDeleteContextClick = () => { - dispatch(deleteContext(contextInput)); + dispatch(deleteContext({ name: currentContext })); setContextInput(''); - // setState(allContext); - setTableState(defaultTableData); - - dispatch(deleteElement({id:'FAKE_ID', contextParam: state})) - // dispatchContext({ - // type: 'DELETE ELEMENT', - // payload: 'FAKE_ID' - // }); + setCurrentContext(null); }; - //re-render data table when there's new changes - const renderTable = targetContext => { - if ( - targetContext === null || - targetContext === undefined || - !targetContext.values - ) { - // if (targetContext === null || targetContext === undefined) { - setTableState(defaultTableData); - } else { - setTableState(targetContext.values); - } - }; + console.log('state.allContext', state.allContext); return ( <> @@ -99,9 +100,13 @@ const CreateContainer = () => { contextStore={state} handleClickSelectContext={handleClickSelectContext} handleDeleteContextClick={handleDeleteContextClick} - renderTable={renderTable} contextInput={contextInput} setContextInput={setContextInput} + currentContext={currentContext} + setCurrentContext={setCurrentContext} + errorStatus={errorStatus} + setErrorStatus={setErrorStatus} + errorMsg={errorMsg} /> @@ -109,7 +114,7 @@ const CreateContainer = () => { @@ -123,11 +128,14 @@ const CreateContainer = () => { > Context Data Table - + ); }; -export default CreateContainer; \ No newline at end of file +export default CreateContainer; diff --git a/app/src/components/ContextAPIManager/CreateTab/components/AddContextForm.tsx b/app/src/components/ContextAPIManager/CreateTab/components/AddContextForm.tsx index 1c96304c2..2312d0002 100644 --- a/app/src/components/ContextAPIManager/CreateTab/components/AddContextForm.tsx +++ b/app/src/components/ContextAPIManager/CreateTab/components/AddContextForm.tsx @@ -1,121 +1,144 @@ -import React, { Fragment, useState, useEffect, useContext } from 'react'; +import React, { Fragment, useState } from 'react'; import TextField from '@mui/material/TextField'; -import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete'; +import Select from '@mui/material/Select'; +import Snackbar from '@mui/material/Snackbar'; import Button from '@mui/material/Button'; import Box from '@mui/material/Box'; -import { Typography } from '@mui/material'; +import FormControl from '@mui/material/FormControl'; +import MuiAlert, { AlertProps } from '@mui/material/Alert'; +import { InputLabel, MenuItem, Typography } from '@mui/material'; import { useSelector } from 'react-redux'; - -const filter = createFilterOptions(); +import { RootState } from '../../../../redux/store'; const AddContextForm = ({ contextStore, handleClickSelectContext, handleDeleteContextClick, - renderTable, contextInput, - setContextInput + setContextInput, + currentContext, + setCurrentContext, + errorMsg, + errorStatus, + setErrorStatus }) => { const { allContext } = contextStore; + console.log('all contexts', allContext); const [btnDisabled, setBtnDisabled] = useState(false); - // const [state, dispatch] = useContext(StateContext); - const { state, isDarkMode } = useSelector(store => ({ + const [open, setOpen] = useState(false); + const { state, isDarkMode } = useSelector((store: RootState) => ({ isDarkMode: store.darkMode.isDarkMode, state: store.appState - })) -const color = isDarkMode ? 'white' : 'black' + })); + const color = isDarkMode ? 'white' : 'black'; - const handleClick = () => { - if (contextInput === '' || contextInput === null) return; + //handler for submitting new context for creation + const handleSubmit = () => { handleClickSelectContext(); + setOpen(true); }; - const onChange = (event, newValue) => { - if (typeof newValue === 'string') { - setContextInput({ - name: newValue - }); - } else if (newValue && newValue.inputValue) { - // Create a new contextInput from the user input - setContextInput({ - name: newValue.inputValue, - values: [] - }); - renderTable(newValue); - } else { - setContextInput(newValue); - renderTable(newValue); - } + //form control for new context field + const handleChange = (e) => { + setErrorStatus(false); + setOpen(false); + setContextInput(e.target.value); }; - const filterOptions = (options, params) => { - // setBtnDisabled(true); - const filtered = filter(options, params); - const { inputValue } = params; - // Suggest the creation of a new contextInput - const isExisting = options.some(option => inputValue === option.name); - if (inputValue !== '' && !isExisting) { - filtered.push({ - inputValue, - name: `Add "${inputValue}"` - }); - - // setBtnDisabled(false); + //event handle for confirmation modal + const handleClose = ( + event: React.SyntheticEvent | Event, + reason?: string + ) => { + if (reason === 'clickaway') { + return; } - return filtered; + setOpen(false); }; - const getOptionLabel = option => { - // Value selected with enter, right from the input - if (typeof option === 'string') { - return option; - } - // Add "xxx" option created dynamically - if (option.inputValue) { - return option.inputValue; - } - // Regular option - return option.name; - }; + const Alert = React.forwardRef(function Alert( + props, + ref + ) { + return ; + }); - const renderOption = (props, option) =>
  • {option.name}
  • ; + //creating options for context dropdown + const contexts = allContext.length ? ( + allContext.map((context) => { + return ( + + {context.name} + + ); + }) + ) : ( + No Contexts Created + ); return ( - Context Input + Create Context - ( - - )} + helperText={errorStatus ? errorMsg : null} + error={errorStatus} + variant="filled" /> + + + Context Created + + + + + Select Context + + + + Select Context + + - {/* */} ); }; -export default AddContextForm; \ No newline at end of file +export default AddContextForm; diff --git a/app/src/components/ContextAPIManager/CreateTab/components/AddDataForm.tsx b/app/src/components/ContextAPIManager/CreateTab/components/AddDataForm.tsx index f51af0ffe..a3dfad07b 100644 --- a/app/src/components/ContextAPIManager/CreateTab/components/AddDataForm.tsx +++ b/app/src/components/ContextAPIManager/CreateTab/components/AddDataForm.tsx @@ -1,23 +1,28 @@ -import React, { Fragment, useState, useEffect } from 'react'; +import React from 'react'; import TextField from '@mui/material/TextField'; import Button from '@mui/material/Button'; import Box from '@mui/material/Box'; import { Typography } from '@mui/material'; -import {useSelector} from 'react-redux' +import { useSelector } from 'react-redux'; +import { RootState } from '../../../../redux/store'; -const AddDataForm = ({ handleClickInputData, contextInput }) => { +const AddDataForm = ({ handleClickInputData, currentContext }) => { //const [contextInput, setContextInput] = React.useState(null); - const defaultInputData = {inputKey: '', inputValue: ''}; + const defaultInputData = { inputKey: '', inputValue: '' }; const [dataContext, setDataContext] = React.useState(defaultInputData); -const {isDarkMode} = useSelector(store=> store.darkMode.isDarkMode) + const { isDarkMode } = useSelector((store: RootState) => store.darkMode); const saveData = () => { setDataContext(defaultInputData); - handleClickInputData(contextInput, dataContext) - } -const color = isDarkMode ? 'white' : 'black'; + if (dataContext.inputKey === '' || dataContext.inputValue === '') { + window.alert('empty key or value'); + return; + } + handleClickInputData(currentContext, dataContext); + }; + const color = isDarkMode ? 'white' : 'black'; - const handleChange = e => { - setDataContext(prevDataContext => { + const handleChange = (e) => { + setDataContext((prevDataContext) => { return { ...prevDataContext, [e.target.name]: e.target.value @@ -26,40 +31,37 @@ const color = isDarkMode ? 'white' : 'black'; }; return ( - + <> - Add context data + Add context data - + handleChange(e)} - InputProps={{style: { color: color }}} - style={{border:'1px solid black'}} + onChange={(e) => handleChange(e)} + InputProps={{ style: { color: color } }} + style={{ border: '1px solid black' }} /> handleChange(e)} - style={{border:'1px solid black'}} - InputProps={{ style: { color: color }}} + onChange={(e) => handleChange(e)} + style={{ border: '1px solid black' }} + InputProps={{ style: { color: color } }} /> - - + ); }; -export default AddDataForm; \ No newline at end of file +export default AddDataForm; diff --git a/app/src/components/ContextAPIManager/CreateTab/components/DataTable.tsx b/app/src/components/ContextAPIManager/CreateTab/components/DataTable.tsx index 2233eaeaa..8dbab721b 100644 --- a/app/src/components/ContextAPIManager/CreateTab/components/DataTable.tsx +++ b/app/src/components/ContextAPIManager/CreateTab/components/DataTable.tsx @@ -7,12 +7,11 @@ import TableRow from '@mui/material/TableRow'; import Paper from '@mui/material/Paper'; import { styled } from '@mui/material/styles'; import TableCell, { tableCellClasses } from '@mui/material/TableCell'; -import TableFooter from '@mui/material/TableFooter'; const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { backgroundColor: theme.palette.common.black, - color: theme.palette.common.white + color: 'theme.palette.common.white' }, [`&.${tableCellClasses.body}`]: { fontSize: 14 @@ -29,7 +28,7 @@ const StyledTableRow = styled(TableRow)(({ theme }) => ({ } })); -export default function DataTable({ target, contextInput }) { +export default function DataTable({ target, currentContext }) { return ( <> @@ -40,29 +39,29 @@ export default function DataTable({ target, contextInput }) { > - {/* Key */} - {contextInput ? contextInput.name : 'Context Name'} + {currentContext ? currentContext : 'Context Name'} {target.map((data, index) => ( - + {data.key} - {data.value} + + {data.value} + ))} - {/* - - {contextInput ? contextInput.name : 'Context Name'} - - */} ); -} \ No newline at end of file +} diff --git a/app/src/components/ContextAPIManager/DisplayTab/DisplayContainer.tsx b/app/src/components/ContextAPIManager/DisplayTab/DisplayContainer.tsx index ebfee3aff..dfcc68f8d 100644 --- a/app/src/components/ContextAPIManager/DisplayTab/DisplayContainer.tsx +++ b/app/src/components/ContextAPIManager/DisplayTab/DisplayContainer.tsx @@ -1,37 +1,42 @@ import React, { useEffect, useState } from 'react'; -import { useStore } from 'react-redux'; +import { useSelector } from 'react-redux'; import { Chart } from 'react-google-charts'; import Grid from '@mui/material/Grid'; +import { RootState } from '../../../redux/store'; const DisplayContainer = () => { - const store = useStore(); - const { allContext } = store.getState().contextSlice; + const allContext = useSelector( + (store: RootState) => store.contextSlice.allContext + ); const [contextData, setContextData] = useState([]); - //build data for Google charts, tree rendering useEffect(() => { - transformData(allContext); + transformData(); }, []); - const transformData = contexts => { - const formattedData = contexts - .map(el => { - return el.components.map(component => { - return [`App - ${el.name} - ${component}`]; + //formats context data for use in react google charts + const transformData = () => { + const formattedData = allContext + .map((obj) => { + return obj.components.map((component) => { + return [`App ⎯⎯ ${obj.name} ⎯⎯ ${component}`]; }); }) .flat(); setContextData([['Phrases'], ...formattedData]); }; + //format options for google chart const options = { wordtree: { format: 'implicit', word: 'App' } }; + return ( + {contextData.length < 2 &&

    No Contexts consumed

    } { ); }; -export default DisplayContainer; \ No newline at end of file +export default DisplayContainer; diff --git a/app/src/components/StateManagement/CreateTab/components/StatePropsPanel.tsx b/app/src/components/StateManagement/CreateTab/components/StatePropsPanel.tsx index 6925a452c..c217a0f07 100644 --- a/app/src/components/StateManagement/CreateTab/components/StatePropsPanel.tsx +++ b/app/src/components/StateManagement/CreateTab/components/StatePropsPanel.tsx @@ -1,8 +1,8 @@ -import React, { useState, useContext, useEffect } from "react"; -import { styled, Theme } from "@mui/material/styles"; +import React, { useState, useEffect } from 'react'; +import { Theme } from '@mui/material/styles'; import makeStyles from '@mui/styles/makeStyles'; import { useDispatch, useSelector } from 'react-redux'; -import { addState } from "../../../../redux/reducers/slice/appStateSlice"; +import { addState } from '../../../../redux/reducers/slice/appStateSlice'; import { FormControl, FormHelperText, @@ -11,23 +11,22 @@ import { Select, TextField, Button -} from "@mui/material"; -import StateContext from "../../../../context/context"; -import TableStateProps from "./TableStateProps"; -import TableParentProps from "./TableParentProps"; -import TablePassedInProps from "./TablePassedInProps"; +} from '@mui/material'; +import TableStateProps from './TableStateProps'; +import TableParentProps from './TableParentProps'; +import TablePassedInProps from './TablePassedInProps'; +import { RootState } from '../../../../redux/store'; - -const StatePropsPanel = ({ isThemeLight, data}): JSX.Element => { - const { state, contextParam } = useSelector((store) => ({ +const StatePropsPanel = ({ isThemeLight, data }): JSX.Element => { + const { state, contextParam } = useSelector((store: RootState) => ({ state: store.appState, - contextParam: store.contextSlice, + contextParam: store.contextSlice })); const dispatch = useDispatch(); const classes = useStyles(); - const [inputKey, setInputKey] = useState(""); - const [inputValue, setInputValue] = useState(""); - const [inputType, setInputType] = useState(""); + const [inputKey, setInputKey] = useState(''); + const [inputValue, setInputValue] = useState(''); + const [inputType, setInputType] = useState(''); const [errorStatus, setErrorStatus] = useState(false); const [errorMsg, setErrorMsg] = useState(''); const currentId = state.canvasFocus.componentId; @@ -40,17 +39,17 @@ const StatePropsPanel = ({ isThemeLight, data}): JSX.Element => { const [propNum, setPropNum] = useState(1); // convert value to correct type based on user input - const typeConversion = (value, type) => { + const typeConversion = (value: string, type: string) => { switch (type) { - case "string": + case 'string': return String(value); - case "number": + case 'number': return Number(value); - case "boolean": - return Boolean(value); - case "array": + case 'boolean': + return value === 'true'; + case 'array': return JSON.parse(value); - case "object": + case 'object': return JSON.parse(value); default: return value; @@ -59,21 +58,17 @@ const StatePropsPanel = ({ isThemeLight, data}): JSX.Element => { // clears the input key, value, and type on Form const clearForm = () => { - setInputKey(""); - setInputValue(""); - setInputType(""); - }; - //reset error warning - const resetError = () => { - setErrorStatus(false); + setInputKey(''); + setInputValue(''); + setInputType(''); }; // submit new stateProps entries to state context const submitNewState = (e) => { e.preventDefault(); - // don't allow them to submit state without all fields - if (!inputKey|| !inputType || !inputValue) { + // don't allow them to submit state without all fields + if (!inputKey || !inputType || !inputValue) { setErrorStatus(true); setErrorMsg('All fields are required'); return; @@ -82,41 +77,49 @@ const StatePropsPanel = ({ isThemeLight, data}): JSX.Element => { const statesArray = currentComponent.stateProps; //loop though array, access each obj at key property let keyToInt = parseInt(inputKey[0]); - if(!isNaN(keyToInt)) { + if (!isNaN(keyToInt)) { setErrorStatus(true); setErrorMsg('Key name can not start with int.'); return; } - // check here to see if state has already been created with the submitted key + // check here to see if state has already been created with the submitted key for (let i = 0; i < state.components.length; i++) { for (let j = 0; j < state.components[i].stateProps.length; j++) { - if (inputKey === state.components[i].stateProps[j]["key"]) { + if (inputKey === state.components[i].stateProps[j]['key']) { setErrorStatus(true); setErrorMsg('Key name already in use.'); return; } } } - setPropNum(prev => prev + 1); + setPropNum((prev) => prev + 1); const newState = { // id name of state will be the parent component name concated with propNum. it will start at 1 and increase by 1 for each new state added id: `${currentComponent.name}-${inputKey}`, key: inputKey, value: typeConversion(inputValue, inputType), - type: inputType, + type: inputType }; const setNewState = { // id name of state will be the parent component name concated with propNum. it will start at 1 and increase by 1 for each new state added - id: `${currentComponent.name}-set${inputKey.slice(0,1).toUpperCase()}${inputKey.slice(1)}`, - key: `set${inputKey.slice(0,1).toUpperCase()}${inputKey.slice(1)}`, + id: `${currentComponent.name}-set${inputKey + .slice(0, 1) + .toUpperCase()}${inputKey.slice(1)}`, + key: `set${inputKey.slice(0, 1).toUpperCase()}${inputKey.slice(1)}`, value: '', - type: 'func', + type: 'func' }; - dispatch(addState({newState: newState, setNewState: setNewState, contextParam: contextParam})) - setRows1([...rows1, newState]) - resetError(); + dispatch( + addState({ + newState: newState, + setNewState: setNewState, + contextParam: contextParam + }) + ); + setRows1([...rows1, newState]); + setErrorStatus(false); clearForm(); }; @@ -134,44 +137,51 @@ const StatePropsPanel = ({ isThemeLight, data}): JSX.Element => { setInputValue(table.row.value); } else clearForm(); }; - //use effect to populate parent props table on load and every time canvas focus changes + //use effect to populate parent props table on load and every time canvas focus changes useEffect(() => { - const parentInfo = findParent(currentId) - + const parentInfo = findParent(currentId); + setParentProps(parentInfo.parentProps); setParentName(parentInfo.parentName); setParentComponent(parentInfo.parentComponent); - setParentPassedInProps(parentInfo.parentPassedInProps) + setParentPassedInProps(parentInfo.parentPassedInProps); }, [currentId]); const findParent = (childId) => { let arr = []; - - for (let i = 0; i < data.length; i++){ - let currComponent = data[i] + + for (let i = 0; i < data.length; i++) { + let currComponent = data[i]; for (let j = 0; j < currComponent.children.length; j++) { let currChild = currComponent.children[j]; if (currChild.typeId === childId) { const currComponentCopy = JSON.parse(JSON.stringify(currComponent)); - return {parentProps: currComponentCopy.stateProps, - parentName: currComponentCopy.name, - parentComponent: currComponentCopy, - parentPassedInProps: currComponentCopy.passedInProps - } + return { + parentProps: currComponentCopy.stateProps, + parentName: currComponentCopy.name, + parentComponent: currComponentCopy, + parentPassedInProps: currComponentCopy.passedInProps + }; } } } - return {parentProps: [], - parentName: '' - } - } + return { parentProps: [], parentName: '' }; + }; return (
    -

    Create New State

    +

    + Create New State +

    { error={errorStatus} onChange={(e) => setInputKey(e.target.value)} helperText={errorStatus ? errorMsg : ''} - className={isThemeLight ? `${classes.rootLight} ${classes.inputTextLight}` : `${classes.rootDark} ${classes.inputTextDark}`} - /> + className={ + isThemeLight + ? `${classes.rootLight} ${classes.inputTextLight}` + : `${classes.rootDark} ${classes.inputTextDark}` + } + /> setInputValue(e.target.value)} - className={isThemeLight ? `${classes.rootLight} ${classes.inputTextLight}` : `${classes.rootDark} ${classes.inputTextDark}`} - /> - + className={ + isThemeLight + ? `${classes.rootLight} ${classes.inputTextLight}` + : `${classes.rootDark} ${classes.inputTextDark}` + } + /> + - Type + className={ + isThemeLight + ? classes.greyThemeFontColor + : classes.darkThemeFontColor + } + > + Type - Required + className={ + isThemeLight + ? classes.greyThemeFontColor + : classes.darkThemeFontColor + } + > + Required

    -
    -
    -
    -
    -

    - Current Component State: {state.components[state.canvasFocus.componentId - 1].name} +
    +
    +
    +

    + Current Component State:{' '} + {state.components[state.canvasFocus.componentId - 1].name}

    - +
    - -
    -

    - Available Props from Parent: {parentName ? parentName : 'No Parents'} + +
    +

    + Available Props from Parent:{' '} + {parentName ? parentName : 'No Parents'}

    - +
    -
    - - +
    + +
    -
    -

    - Passed in Props from Parent: {parentName ? parentName : 'No Parents'} +
    +

    + Passed in Props from Parent:{' '} + {parentName ? parentName : 'No Parents'}

    - +

    ); }; -const useStyles = makeStyles((theme: Theme) => - ({ - inputField: { - marginTop: "10px", - borderRadius: "5px", - whiteSpace: "nowrap", - overflowX: "hidden", - textOverflow: "ellipsis", - backgroundColor: "rgba(255,255,255,0.15)", - margin: "0px 0px 0px 10px", - width: "140px", - height: "30px", - borderColor: "white", - }, - inputWrapper: { - textAlign: "center", - display: "flex", - flexDirection: "column", - alignItems: "center", - justifyContent: "space-between", - marginBottom: "15px", - }, - addComponentWrapper: { - padding: "auto", - marginLeft: "21px", - display: "inline-block", - width: "100%", - }, - rootCheckBox: { - borderColor: "#186BB4", - padding: "0px", - }, - rootCheckBoxLabel: { - borderColor: "#186BB4", - }, - panelWrapper: { - width: "100%", - marginTop: "15px", - display: "flex", - flexDirection: "column", - alignItems: "center", - }, - panelWrapperList: { - minHeight: "120px", - marginLeft: "-15px", - marginRight: "-15px", - width: "300px", - display: "flex", - flexDirection: "column", - alignItems: "center", - }, - dragComponents: { - display: "flex", - flexDirection: "column", - alignItems: "center", - textAlign: "center", - width: "500px", - backgroundColor: "#186BB4", - border: "5px solid #186BB4", - }, - panelSubheader: { - textAlign: "center", - color: "#fff", - }, - input: {}, - newComponent: { - color: "#155084", - fontSize: "95%", - marginBottom: "20px", - }, - inputLabel: { - fontSize: "1em", - marginLeft: "10px", - }, - btnGroup: { - display: "flex", - flexDirection: "column", - }, - addComponentButton: { - backgroundColor: "transparent", - height: "40px", - width: "100px", - fontFamily: 'Roboto, Raleway, sans-serif', - fontSize: "90%", - textAlign: "center", - margin: "-20px 0px 5px 150px", - borderStyle: "none", - transition: "0.3s", - // borderRadius: "25px", - }, - rootToggle: { - color: "#696969", - fontSize: "0.85rem", - }, - lightThemeFontColor: { - color: "#155084", - }, - darkThemeFontColor: { - color: "#fff", - }, - greyThemeFontColor: { - color: 'rgba(0,0,0,0.54)', - }, - formControl: { - margin: theme.spacing(1), - minWidth: 120, - }, - selectEmpty: { - marginTop: theme.spacing(2), - }, - color: { - color: '#fff', - }, - rootLight: { - '& .MuiFormLabel-root': { - color: 'rgba(0,0,0,0.54)' - } - }, - rootDark: { - '& .MuiFormLabel-root': { - color: '#fff' - }, - '& .MuiOutlinedInput-notchedOutline': { - borderColor: '#fff' - } - }, - underlineDark: { - borderBottom: '1px solid white' - }, - rootUnderlineDark: { - '& .-icon': { - color: '#fff', - }, - '&::before': { - borderBottom: '1px solid #fff' - } +const useStyles = makeStyles((theme: Theme) => ({ + inputField: { + marginTop: '10px', + borderRadius: '5px', + whiteSpace: 'nowrap', + overflowX: 'hidden', + textOverflow: 'ellipsis', + backgroundColor: 'rgba(255,255,255,0.15)', + margin: '0px 0px 0px 10px', + width: '140px', + height: '30px', + borderColor: 'white' + }, + inputWrapper: { + textAlign: 'center', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: '15px' + }, + addComponentWrapper: { + padding: 'auto', + marginLeft: '21px', + display: 'inline-block', + width: '100%' + }, + rootCheckBox: { + borderColor: '#186BB4', + padding: '0px' + }, + rootCheckBoxLabel: { + borderColor: '#186BB4' + }, + panelWrapper: { + width: '100%', + marginTop: '15px', + display: 'flex', + flexDirection: 'column', + alignItems: 'center' + }, + panelWrapperList: { + minHeight: '120px', + marginLeft: '-15px', + marginRight: '-15px', + width: '300px', + display: 'flex', + flexDirection: 'column', + alignItems: 'center' + }, + dragComponents: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + textAlign: 'center', + width: '500px', + backgroundColor: '#186BB4', + border: '5px solid #186BB4' + }, + panelSubheader: { + textAlign: 'center', + color: '#fff' + }, + input: {}, + newComponent: { + color: '#155084', + fontSize: '95%', + marginBottom: '20px' + }, + inputLabel: { + fontSize: '1em', + marginLeft: '10px' + }, + btnGroup: { + display: 'flex', + flexDirection: 'column' + }, + addComponentButton: { + backgroundColor: 'transparent', + height: '40px', + width: '100px', + fontFamily: 'Roboto, Raleway, sans-serif', + fontSize: '90%', + textAlign: 'center', + margin: '-20px 0px 5px 150px', + borderStyle: 'none', + transition: '0.3s' + // borderRadius: "25px", + }, + rootToggle: { + color: '#696969', + fontSize: '0.85rem' + }, + lightThemeFontColor: { + color: '#155084' + }, + darkThemeFontColor: { + color: '#fff' + }, + greyThemeFontColor: { + color: 'rgba(0,0,0,0.54)' + }, + formControl: { + margin: '8px', + minWidth: 120 + }, + selectEmpty: { + marginTop: '16px' + }, + color: { + color: '#fff' + }, + rootLight: { + '& .MuiFormLabel-root': { + color: 'rgba(0,0,0,0.54)' + } + }, + rootDark: { + '& .MuiFormLabel-root': { + color: '#fff' }, - rootUnderlineLight: { - '& .-icon': { - color: 'rgba(0,0,0,0.54)', - }, - '&::before': { - borderBottom: '1px solid rgba(0,0,0,0.54)' - } + '& .MuiOutlinedInput-notchedOutline': { + borderColor: '#fff' + } + }, + underlineDark: { + borderBottom: '1px solid white' + }, + rootUnderlineDark: { + '& .-icon': { + color: '#fff' }, - inputTextDark: { - '& .MuiInputBase-input': { - color: 'white' - } + '&::before': { + borderBottom: '1px solid #fff' + } + }, + rootUnderlineLight: { + '& .-icon': { + color: 'rgba(0,0,0,0.54)' }, - inputTextLight: { - '& .MuiInputBase-input': { - color: 'rgba(0,0,0,0.54)' - } + '&::before': { + borderBottom: '1px solid rgba(0,0,0,0.54)' } - }) -); + }, + inputTextDark: { + '& .MuiInputBase-input': { + color: 'white' + } + }, + inputTextLight: { + '& .MuiInputBase-input': { + color: 'rgba(0,0,0,0.54)' + } + } +})); export default StatePropsPanel; diff --git a/app/src/components/StateManagement/CreateTab/components/TableParentProps.tsx b/app/src/components/StateManagement/CreateTab/components/TableParentProps.tsx index c2fa2fd4c..7ef91d055 100644 --- a/app/src/components/StateManagement/CreateTab/components/TableParentProps.tsx +++ b/app/src/components/StateManagement/CreateTab/components/TableParentProps.tsx @@ -1,18 +1,17 @@ -import React, { useState, useContext, useEffect } from 'react'; +import React, { useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { DataGrid, GridEditRowsModel, } from '@mui/x-data-grid'; import Button from '@mui/material/Button'; -import StateContext from "../../../../context/context"; import makeStyles from '@mui/styles/makeStyles'; -import { StatePropsPanelProps } from '../../../../interfaces/Interfaces'; import AddIcon from '@mui/icons-material/Add'; import { addPassedInProps } from '../../../../redux/reducers/slice/appStateSlice'; +import { RootState } from '../../../../redux/store' const TableParentProps = props => { - const { state, contextParam } = useSelector((store) => ({ + const { state, contextParam } = useSelector((store:RootState) => ({ state: store.appState, contextParam: store.contextSlice, })); @@ -121,6 +120,12 @@ const TableParentProps = props => { const useStyles = makeStyles({ themeLight: { color: 'rgba(0,0,0,0.54)', + '& button:hover':{ + backgroundColor: 'LightGray' + }, + '& button':{ + color: 'black' + }, '& .MuiTablePagination-root': { color: 'rbga(0,0,0,0.54)' } diff --git a/app/src/components/StateManagement/CreateTab/components/TablePassedInProps.tsx b/app/src/components/StateManagement/CreateTab/components/TablePassedInProps.tsx index 11fcd3a80..8a2c61107 100644 --- a/app/src/components/StateManagement/CreateTab/components/TablePassedInProps.tsx +++ b/app/src/components/StateManagement/CreateTab/components/TablePassedInProps.tsx @@ -8,10 +8,11 @@ import ClearIcon from '@mui/icons-material/Clear'; import makeStyles from '@mui/styles/makeStyles'; import { useDispatch, useSelector } from 'react-redux'; import { deletePassedInProps } from '../../../../redux/reducers/slice/appStateSlice'; +import { RootState } from '../../../../redux/store' const TablePassedInProps = props => { - const { state, contextParam } = useSelector((store) => ({ + const { state, contextParam } = useSelector((store:RootState) => ({ state: store.appState, contextParam: store.contextSlice, })); @@ -92,7 +93,8 @@ const TablePassedInProps = props => { }, [state.canvasFocus.componentId]); // fill data grid rows with all of the passed in props from parent component (if there are any) - let rows = passedInProps?.slice(); + let rows: any = passedInProps?.slice(); + //let rows: readonly StateProp[] = passedInProps?.slice() || []; return (
    @@ -106,10 +108,16 @@ const TablePassedInProps = props => {
    ); }; - +// colors of state mgmt modal const useStyles = makeStyles({ themeLight: { color: 'rgba(0,0,0,0.54)', + '& button:hover':{ + backgroundColor: 'LightGray' + }, + '& button':{ + color: 'black' + }, '& .MuiTablePagination-root': { color: 'rbga(0,0,0,0.54)' } diff --git a/app/src/components/StateManagement/CreateTab/components/TableStateProps.tsx b/app/src/components/StateManagement/CreateTab/components/TableStateProps.tsx index 3307deee3..a9c004430 100644 --- a/app/src/components/StateManagement/CreateTab/components/TableStateProps.tsx +++ b/app/src/components/StateManagement/CreateTab/components/TableStateProps.tsx @@ -1,17 +1,19 @@ import React, { useState, useEffect } from 'react'; import { DataGrid, - GridEditRowsModel, + GridEditRowsModel } from '@mui/x-data-grid'; import Button from '@mui/material/Button'; import ClearIcon from '@mui/icons-material/Clear'; import makeStyles from '@mui/styles/makeStyles'; import { StatePropsPanelProps } from '../../../../interfaces/Interfaces'; import { useDispatch, useSelector } from 'react-redux'; -import { deletePassedInProps } from '../../../../redux/reducers/slice/appStateSlice'; +import { deleteState } from '../../../../redux/reducers/slice/appStateSlice'; +import { RootState } from '../../../../redux/store' +// updates state mgmt boxes and data grid const TableStateProps = props => { - const { state, contextParam } = useSelector((store) => ({ + const { state, contextParam } = useSelector((store:RootState) => ({ state: store.appState, contextParam: store.contextSlice, })); @@ -58,7 +60,7 @@ const TableStateProps = props => {

    ); @@ -128,8 +117,11 @@ const TableStateProps = props => { const useStyles = makeStyles({ themeLight: { color: 'rgba(0,0,0,0.54)', - '& .MuiTablePagination-root': { - color: 'rbga(0,0,0,0.54)' + '& button:hover':{ + backgroundColor: 'LightGray' + }, + '& button':{ + color: 'black' } }, themeDark: { diff --git a/app/src/components/StateManagement/DisplayTab/DataTable.tsx b/app/src/components/StateManagement/DisplayTab/DataTable.tsx index 081224173..453dbd2e8 100644 --- a/app/src/components/StateManagement/DisplayTab/DataTable.tsx +++ b/app/src/components/StateManagement/DisplayTab/DataTable.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React from 'react'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableContainer from '@mui/material/TableContainer'; @@ -7,8 +7,8 @@ import TableRow from '@mui/material/TableRow'; import Paper from '@mui/material/Paper'; import { styled } from '@mui/material/styles'; import TableCell, { tableCellClasses } from '@mui/material/TableCell'; -import StateContext from '../../../context/context'; import { useSelector } from 'react-redux'; +import { RootState } from '../../../redux/store' const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { @@ -16,13 +16,14 @@ const StyledTableCell = styled(TableCell)(({ theme }) => ({ color: theme.palette.common.white, }, [`&.${tableCellClasses.body}`]: { + color: theme.palette.common.black, fontSize: 14, }, })); const StyledTableRow = styled(TableRow)(({ theme }) => ({ '&:nth-of-type(odd)': { - backgroundColor: theme.palette.action.hover, + backgroundColor: theme.palette.action.hover }, // hide last border '&:last-child td, &:last-child th': { @@ -34,7 +35,7 @@ export default function DataTable(props) { const { currComponentState, parentProps, clickedComp, data, } = props; - const state = useSelector(store => store.appState) + const state = useSelector((store:RootState) => store.appState) // determine if the current component is a root component let isRoot = false; diff --git a/app/src/components/StateManagement/DisplayTab/DisplayContainer.tsx b/app/src/components/StateManagement/DisplayTab/DisplayContainer.tsx index a4cb610b7..082a27698 100644 --- a/app/src/components/StateManagement/DisplayTab/DisplayContainer.tsx +++ b/app/src/components/StateManagement/DisplayTab/DisplayContainer.tsx @@ -1,17 +1,17 @@ -import React, { useState, useContext } from 'react'; +import React, { useState } from 'react'; import Divider from '@mui/material/Divider'; import Grid from '@mui/material/Grid'; import { Typography } from '@mui/material'; import DataTable from './DataTable'; import Tree from './Tree'; -import StateContext from '../../../context/context'; import { useSelector } from 'react-redux'; +import { RootState } from '../../../redux/store' function DisplayContainer({ data, props }) { // "data" is referring to components from state - passed in from StateManagement // grabbing intialized state from App using UseContext const [currComponentState, setCurrComponentState] = useState([]); const [parentProps, setParentProps] = useState([]); - const state = useSelector(store => store.appState) + const state = useSelector((store:RootState) => store.appState) let root = ''; @@ -33,7 +33,7 @@ function DisplayContainer({ data, props }) { // "data" is referring to component return (
    - + store.appState) - const svgRef = useRef(); - const wrapperRef = useRef(); + const state = useSelector((store:RootState) => store.appState) + // Provide types for the refs. + // In this case HTMLDivElement for the wrapperRef and SVGSVGElement for the svgRef. + // create mutable ref objects with initial values of null + const svgRef = useRef(null); + const wrapperRef = useRef(null); const xPosition = 50; const textAndBorderColor = '#bdbdbd'; const dimensions = useResizeObserver(wrapperRef); // we save data to see if it changed const previouslyRenderedData = usePrevious(data); // function to filter out separators to prevent render on tree chart - const removeHTMLElements = (arr: object[]) => { + const removeHTMLElements = (arr: ChildElement[]) => { for (let i = 0; i < arr.length; i++) { if (arr[i] === undefined) continue; // if element is separator, remove it @@ -67,6 +72,7 @@ function Tree({ // use dimensions from useResizeObserver, // but use getBoundingClientRect on initial render // (dimensions are null for the first render) + const { width, height } = dimensions || wrapperRef.current.getBoundingClientRect(); // transform hierarchical data diff --git a/app/src/components/StateManagement/DisplayTab/useResizeObserver.js b/app/src/components/StateManagement/DisplayTab/useResizeObserver.ts similarity index 100% rename from app/src/components/StateManagement/DisplayTab/useResizeObserver.js rename to app/src/components/StateManagement/DisplayTab/useResizeObserver.ts diff --git a/app/src/components/StateManagement/StateManagement.tsx b/app/src/components/StateManagement/StateManagement.tsx index 103c172f5..5be1d9770 100644 --- a/app/src/components/StateManagement/StateManagement.tsx +++ b/app/src/components/StateManagement/StateManagement.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React from 'react'; import { makeStyles } from '@mui/styles'; import Box from '@mui/material/Box'; import Tab from '@mui/material/Tab'; @@ -6,9 +6,9 @@ import TabContext from '@mui/lab/TabContext'; import TabList from '@mui/lab/TabList'; import TabPanel from '@mui/lab/TabPanel'; import { useSelector } from 'react-redux'; - import CreateContainer from './CreateTab/CreateContainer'; import DisplayContainer from './DisplayTab/DisplayContainer'; +import { RootState } from '../../redux/store'; const useStyles = makeStyles({ contextContainer: { @@ -20,9 +20,10 @@ const useStyles = makeStyles({ }); const StateManager = (props): JSX.Element => { - - const state = useSelector(store => store.appState) - const isDarkMode = useSelector(state => state.darkMode.isDarkMode); + const state = useSelector((store: RootState) => store.appState); + const isDarkMode = useSelector( + (state: RootState) => state.darkMode.isDarkMode + ); const { components } = state; const classes = useStyles(); @@ -32,27 +33,38 @@ const StateManager = (props): JSX.Element => { setValue(newValue); }; - // add hook here to access which component has been clicked - // then this will re-render the dataTable + // add hook here to access which component has been clicked + // then this will re-render the dataTable - const background_Color = isDarkMode ? '#21262b' : 'white' + const background_Color = isDarkMode ? '#21262b' : 'white'; + const color = isDarkMode ? 'white' : 'black'; return ( -
    +
    - - + + - + - + @@ -61,4 +73,4 @@ const StateManager = (props): JSX.Element => { ); }; -export default StateManager; \ No newline at end of file +export default StateManager; diff --git a/app/src/components/bottom/BottomPanel.tsx b/app/src/components/bottom/BottomPanel.tsx index 7141c3fcc..898787129 100644 --- a/app/src/components/bottom/BottomPanel.tsx +++ b/app/src/components/bottom/BottomPanel.tsx @@ -1,4 +1,4 @@ -import React, { RefObject, useEffect, useRef } from 'react'; +import React, { useEffect, useRef } from 'react'; import BottomTabs from './BottomTabs'; const BottomPanel = (props): JSX.Element => { @@ -15,7 +15,7 @@ const BottomPanel = (props): JSX.Element => { document.addEventListener('mousemove', mouseMoveHandler); document.addEventListener('mouseup', mouseUpHandler); - } + }; const mouseMoveHandler = function (e: MouseEvent): void { // How far the mouse has been moved @@ -26,19 +26,21 @@ const BottomPanel = (props): JSX.Element => { }; const mouseUpHandler = function () { - // Remove the handlers of `mousemove` and `mouseup` - document.removeEventListener('mousemove', mouseMoveHandler); - document.removeEventListener('mouseup', mouseUpHandler); + // Remove the handlers of `mousemove` and `mouseup` + document.removeEventListener('mousemove', mouseMoveHandler); + document.removeEventListener('mouseup', mouseUpHandler); }; useEffect(() => { node.current.style.height = '50vh'; - },[]); + }, []); return ( -
    -
    ......
    - +
    +
    + ...... +
    +
    ); }; diff --git a/app/src/components/bottom/BottomTabs.tsx b/app/src/components/bottom/BottomTabs.tsx index 61494c140..4d604f0d8 100644 --- a/app/src/components/bottom/BottomTabs.tsx +++ b/app/src/components/bottom/BottomTabs.tsx @@ -1,4 +1,4 @@ -import React, { useState} from 'react'; +import React, { useState } from 'react'; import makeStyles from '@mui/styles/makeStyles'; import Tabs from '@mui/material/Tabs'; import Tab from '@mui/material/Tab'; @@ -13,15 +13,15 @@ import Tree from '../../tree/TreeChart'; import FormControl from '@mui/material/FormControl'; import MenuItem from '@mui/material/MenuItem'; import Select from '@mui/material/Select'; -import Arrow from '../main/Arrow'; +import arrow from '../main/Arrow'; import { useDispatch, useSelector } from 'react-redux'; import { changeProjectType } from '../../redux/reducers/slice/appStateSlice'; - +import { RootState } from '../../redux/store'; const BottomTabs = (props): JSX.Element => { // state that controls which tab the user is on const dispatch = useDispatch(); - const { state, contextParam, style, } = useSelector((store) => ({ + const { state, contextParam, style } = useSelector((store: RootState) => ({ state: store.appState, contextParam: store.contextSlice, style: store.styleSlice, @@ -37,58 +37,114 @@ const BottomTabs = (props): JSX.Element => { }; // Allows users to toggle project between "next.js" and "Classic React" // When a user changes the project type, the code of all components is rerendered - const handleProjectChange = event => { + const handleProjectChange = (event) => { const projectType = event.target.value; - dispatch(changeProjectType({ projectType, contextParam })) - }; - const { components, HTMLTypes } = state; - - const changeTheme = e => { - setTheme(e.target.value); + dispatch(changeProjectType({ projectType, contextParam })); }; + const { components } = state; // Render's the highliting arrow feature that draws an arrow from the Canvas to the DemoRender - Arrow.renderArrow(state.canvasFocus?.childId); + arrow.renderArrow(state.canvasFocus?.childId); return (
    - - - - - - - - - + + + + + + + + +
    - + + Classic React + + + Gatsby.js + + + Next.js +
    -
    +
    {tab === 0 && } {tab === 1 && } {tab === 2 && } {tab === 3 && } {tab === 4 && } {tab === 5 && } - {tab === 6 && } + {tab === 6 && ( + + )}
    ); }; -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles((theme) => ({ root: { flexGrow: 1, height: '100%', @@ -113,9 +169,8 @@ const useStyles = makeStyles(theme => ({ tabRoot: { textTransform: 'initial', minWidth: 40, - fontWeight: theme.typography.fontWeightRegular, - marginRight: theme.spacing(2), // JZ: updated syntax as per deprecation warning - marginLeft: theme.spacing(2), + // fontWeight: theme.typography.fontWeightRegular, + margin: '0 16px', fontFamily: [ '-apple-system', @@ -134,8 +189,8 @@ const useStyles = makeStyles(theme => ({ opacity: 1 }, '&$tabSelected': { - color: 'white', - fontWeight: theme.typography.fontWeightMedium + color: 'white' + // fontWeight: theme.typography.fontWeightMedium }, '&:focus': { color: 'white' @@ -143,10 +198,10 @@ const useStyles = makeStyles(theme => ({ }, tabSelected: {}, typography: { - padding: theme.spacing(3) + padding: '24px' }, padding: { - padding: `0 ${theme.spacing(2)}` + padding: `0 16px` }, switch: { marginRight: '10px', @@ -155,7 +210,7 @@ const useStyles = makeStyles(theme => ({ projectTypeWrapper: { marginTop: '10px', marginBotton: '10px', - marginLeft: '10px', + marginLeft: '10px' }, projectSelector: { backgroundColor: '#0099E6', diff --git a/app/src/components/bottom/CodePreview.tsx b/app/src/components/bottom/CodePreview.tsx index 376668c4d..0232d7041 100644 --- a/app/src/components/bottom/CodePreview.tsx +++ b/app/src/components/bottom/CodePreview.tsx @@ -15,6 +15,7 @@ import { fetchPlugin } from '../../plugins/fetch-plugin'; import * as esbuild from 'esbuild-wasm'; import {codePreviewSave, codePreviewInput} from "../../redux/reducers/slice/codePreviewSlice"; import { useDispatch, useSelector } from 'react-redux'; +import { RootState } from '../../redux/store'; const CodePreview: React.FC<{ theme: string | null; @@ -36,13 +37,13 @@ const CodePreview: React.FC<{ const wrapper = useRef(); const dimensions = useResizeObserver(wrapper); const { height } = dimensions || 0; - const state = useSelector(store => store.appState) + const state = useSelector((store:RootState) => store.appState) const [, setDivHeight] = useState(0); let currentComponent = state.components.find( (elem: Component) => elem.id === state.canvasFocus.componentId ); - const [input, setInput] = useState(); + const [input, setInput] = useState(''); useEffect(() => { startService(); @@ -102,6 +103,9 @@ const CodePreview: React.FC<{ readOnly={false} fontSize={18} tabSize={2} + setOptions={{ + useWorker: false + }} />
    ); diff --git a/app/src/components/bottom/CreationPanel.tsx b/app/src/components/bottom/CreationPanel.tsx index 81e59dacd..d00616ada 100644 --- a/app/src/components/bottom/CreationPanel.tsx +++ b/app/src/components/bottom/CreationPanel.tsx @@ -1,16 +1,17 @@ import React from 'react'; -import ComponentPanel from '../right/ComponentPanel' -import HTMLPanel from '../left/HTMLPanel' +import ComponentPanel from '../right/ComponentPanel'; +import HTMLPanel from '../left/HTMLPanel'; import { useSelector } from 'react-redux'; +import { RootState } from '../../redux/store'; // Creation panel holds all of the creation functionality of the application. ComponentPanel, HTMLPanel, and StatePropsPanel are all hanged here. // This allows users to create all aspects of this application in one place. const CreationPanel = (props): JSX.Element => { - const style = useSelector((store) => store.styleSlice); + const style = useSelector((store: RootState) => store.styleSlice); return (
    - - + +
    ); }; diff --git a/app/src/components/bottom/StylesEditor.tsx b/app/src/components/bottom/StylesEditor.tsx index 05d1e8fea..23ed0468d 100644 --- a/app/src/components/bottom/StylesEditor.tsx +++ b/app/src/components/bottom/StylesEditor.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState, useRef } from 'react'; import AceEditor from 'react-ace'; import 'ace-builds/src-noconflict/mode-css'; import 'ace-builds/src-noconflict/theme-monokai'; @@ -10,64 +10,42 @@ import 'ace-builds/src-noconflict/theme-monokai'; import 'ace-builds/src-min-noconflict/ext-searchbox'; import Fab from '@mui/material/Fab'; import SaveIcon from '@mui/icons-material/Save'; -import cssRefresher from '../../helperFunctions/cssRefresh'; - -const serverURL = 'https://reactype-caret.herokuapp.com'; +import { updateStylesheet } from '../../redux/reducers/slice/appStateSlice'; +import { useDispatch, useSelector } from 'react-redux'; +import { RootState } from '../../redux/store'; const StylesEditor: React.FC<{ theme: string | null; setTheme: any | null; }> = ({ theme, setTheme }) => { const wrapper = useRef(); - const [css, setCss] = useState(); - - useEffect(() => { - loadFile(); - }, []); + const stylesheet = useSelector( + (state: RootState) => state.appState.stylesheet + ); + //sets state for what text is currently in the csseditor + const [css, setCss] = useState(stylesheet); - const loadFile = () => { - const myHeaders = new Headers({ - 'Content-Type': 'text/css', - Accept: 'text/css', - }); - fetch(`${serverURL}/demoRender`, { - headers: myHeaders, - }) - .then(response => response.text()) - .then((data) => { - setCss(data); - }); - } + const dispatch = useDispatch(); - const saveFile = () => { - fetch(`${serverURL}/user-styles/save`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ data: css }), - }) - .then(response => response.text()) - .then((data) => { - // Removes old link to css and creates a new stylesheet link on demo render - cssRefresher(); - }); - } + //on save, updates the state based on above hook and rerenders the demo const saveCss = (e) => { e.preventDefault(); - saveFile(); - } + dispatch(updateStylesheet(css)); + }; + //handles changes in the ace editor const handleChange = (text) => { setCss(text); - } + }; return (
    - +
    diff --git a/app/src/components/bottom/TableStateProps.tsx b/app/src/components/bottom/TableStateProps.tsx deleted file mode 100644 index 04c92bdb3..000000000 --- a/app/src/components/bottom/TableStateProps.tsx +++ /dev/null @@ -1,147 +0,0 @@ - -import React, { useState, useContext, useEffect } from 'react'; -import { - DataGrid, - GridEditRowsModel, -} from '@mui/x-data-grid'; -import Button from '@mui/material/Button'; -import ClearIcon from '@mui/icons-material/Clear'; -import makeStyles from '@mui/styles/makeStyles'; -import { StatePropsPanelProps } from '../../interfaces/Interfaces'; -import { useDispatch, useSelector } from 'react-redux'; -import { deleteState } from '../../redux/reducers/slice/appStateSlice'; - -const TableStateProps = props => { - const { state, contextParam } = useSelector((store) => ({ - state: store.appState, - contextParam: store.contextSlice, - })); - const dispatch = useDispatch(); - const classes = useStyles(); - const [editRowsModel] = useState({}); - const [gridColumns, setGridColumns] = useState([]); - const columnTabs = [ - { - field: 'id', - headerName: 'ID', - width: 70, - editable: false - }, - { - field: 'key', - headerName: 'Key', - width: 90, - editable: true - }, - { - field: 'value', - headerName: 'Initial Value', - width: 100, - editable: true - }, - { - field: 'type', - headerName: 'Type', - width: 90, - editable: false - }, - { - field: 'delete', - headerName: 'X', - width: 70, - editable: false, - renderCell: function renderCell(params: any) { - return ( - - ); - } - } - ]; - const handleDeleteState = selectedId => { - // get the current focused component - // remove the state that the button is clicked - // send a dispatch to rerender the table - const currentId = state.canvasFocus.componentId; - const currentComponent = state.components[currentId - 1]; - const filtered = currentComponent.stateProps.filter( - element => element.id !== selectedId - ); - dispatch(deleteState({stateProps: filtered, rowId: selectedId, contextParam: contextParam})) - }; - - useEffect(() => { - setGridColumns(columnTabs); - }, [props.isThemeLight]); - - const { selectHandler }: StatePropsPanelProps = props; - // the delete button needs to be updated to remove - // the states from the current focused component - useEffect(() => { - if (props.canDeleteState) { - setGridColumns(columnTabs); - } else { - setGridColumns(columnTabs.slice(0, gridColumns.length - 1)); - } - }, [state.canvasFocus.componentId]); - - // create rows to show the current component's state props - let rows = []; - const currentId = state.canvasFocus.componentId; - const currentComponent = state.components[currentId - 1]; - let currentProps = currentComponent.stateProps.slice(); - - //add in passed in props for the current component (if it is not a root component) - if (currentComponent.name !== 'App' && currentComponent.name !== 'index') { - let propsPassed = currentComponent.passedInProps?.slice(); - for (let i = 0; i < propsPassed.length; i++) { - currentProps.push(propsPassed[i]); - } - } - rows = currentProps; - - return ( -
    - -
    - ); -}; - -const useStyles = makeStyles({ - themeLight: { - color: 'rgba(0,0,0,0.54)', - '& .MuiTablePagination-root': { - color: 'rbga(0,0,0,0.54)' - } - }, - themeDark: { - color: 'white', - '& .MuiTablePagination-root': { - color: 'white' - }, - '& .MuiIconButton-root': { - color: 'white' - }, - '& .MuiSvgIcon-root': { - color: 'white' - }, - '& .MuiDataGrid-window': { - backgroundColor: 'rgba(0,0,0,0.54)' - } - } -}); - -export default TableStateProps; diff --git a/app/src/components/bottom/UseStateModal.tsx b/app/src/components/bottom/UseStateModal.tsx index af71ca300..fc828f118 100644 --- a/app/src/components/bottom/UseStateModal.tsx +++ b/app/src/components/bottom/UseStateModal.tsx @@ -1,12 +1,12 @@ -import React, {useState, useRef} from 'react'; +import React, { useState, useRef } from 'react'; import Modal from '@mui/material/Modal'; -import TableStateProps from './TableStateProps'; +import TableStateProps from '../StateManagement/CreateTab/components/TableStateProps'; -function UseStateModal({ updateAttributeWithState, attributeToChange, childId }) { +function UseStateModal({ updateAttributeWithState, attributeToChange }) { const [open, setOpen] = useState(false); const [stateKey, setStateKey] = useState(''); const [statePropsId, setStatePropsId] = useState(-1); - const [componentProviderId, setComponentProviderId] = useState(1); + const [componentProviderId, setComponentProviderId] = useState(1); const container = useRef(null); // table to choose state from const body = ( @@ -14,27 +14,33 @@ function UseStateModal({ updateAttributeWithState, attributeToChange, childId })
    Choose State +
    { - updateAttributeWithState(attributeToChange, componentProviderId, statePropsId > 0 ? statePropsId : table.row.id, table.row, stateKey + table.row.key); - setStateKey('') - setStatePropsId(-1); - setOpen(false); + updateAttributeWithState( + attributeToChange, + componentProviderId, + statePropsId > 0 ? statePropsId : table.row.id, + table.row, + stateKey + table.row.key + ); + setStateKey(''); + setStatePropsId(-1); + setOpen(false); }} - deleteHandler={() => func()} isThemeLight={true} />
    @@ -44,8 +50,12 @@ function UseStateModal({ updateAttributeWithState, attributeToChange, childId }) return (
    - - {body} + + + {body} +
    ); } diff --git a/app/src/components/form/Selector.tsx b/app/src/components/form/Selector.tsx index c28c32417..91b2ebbe1 100644 --- a/app/src/components/form/Selector.tsx +++ b/app/src/components/form/Selector.tsx @@ -1,4 +1,3 @@ - import React from 'react'; import FormControl from '@mui/material/FormControl'; import Select from '@mui/material/Select'; diff --git a/app/src/components/right/ComponentDrag.tsx b/app/src/components/left/ComponentDrag.tsx similarity index 61% rename from app/src/components/right/ComponentDrag.tsx rename to app/src/components/left/ComponentDrag.tsx index 787690d66..850b85e71 100644 --- a/app/src/components/right/ComponentDrag.tsx +++ b/app/src/components/left/ComponentDrag.tsx @@ -1,13 +1,16 @@ import React from 'react'; import Grid from '@mui/material/Grid'; -import ComponentPanelItem from './ComponentPanelItem'; +import ComponentPanelItem from '../right/ComponentPanelItem'; import makeStyles from '@mui/styles/makeStyles'; import { useSelector } from 'react-redux'; +import { RootState } from '../../redux/store'; // The component panel section of the left panel displays all components and has the ability to add new components const ComponentDrag = ({ isThemeLight }): JSX.Element => { const classes = useStyles(); - const state = useSelector(store => store.appState) - const isDarkMode = useSelector(store => store.darkMode.isDarkMode); + const state = useSelector((store: RootState) => store.appState); + const isDarkMode = useSelector( + (store: RootState) => store.darkMode.isDarkMode + ); const isFocus = (targetId: Number) => { return state.canvasFocus.componentId === targetId ? true : false; }; @@ -18,11 +21,26 @@ const ComponentDrag = ({ isThemeLight }): JSX.Element => { {/* Font size for 'index' in root components in .compPanelItem h3 style.css */}
    {/* Heading just below ADD button */} -

    {state.projectType === 'Next.js' || state.projectType === 'Gatsby.js' ? 'Pages' : 'Root Components'}

    - +

    + {state.projectType === 'Next.js' || state.projectType === 'Gatsby.js' + ? 'Pages' + : 'Root Components'} +

    + {state.components - .filter(comp => state.rootComponents.includes(comp.id)) - .map(comp => { + .filter((comp) => state.rootComponents.includes(comp.id)) + .map((comp) => { return ( { root={true} isThemeLight={isThemeLight} /> - ); })} {/* Display all reusable components */} -

    Reusable Components

    - +

    + Reusable Components +

    + {state.components - .filter(comp => !state.rootComponents.includes(comp.id)) - .map(comp => { + .filter((comp) => !state.rootComponents.includes(comp.id)) + .map((comp) => { return ( { ); })} -
    ); @@ -66,7 +95,7 @@ const useStyles = makeStyles({ flexDirection: 'column', alignItems: 'center', flexGrow: 1, - overflow: 'auto', + overflow: 'auto' }, panelWrapperList: { minHeight: '120px', @@ -76,7 +105,7 @@ const useStyles = makeStyles({ display: 'flex', flexDirection: 'column', alignItems: 'center', - wordWrap: 'break-word', + wordWrap: 'break-word' }, lightThemeFontColor: { color: '#155084' diff --git a/app/src/components/left/DragDropPanel.tsx b/app/src/components/left/DragDropPanel.tsx index 6e5e52888..4b6e30a09 100644 --- a/app/src/components/left/DragDropPanel.tsx +++ b/app/src/components/left/DragDropPanel.tsx @@ -1,7 +1,7 @@ -import React, { useContext } from 'react'; +import React from 'react'; import Grid from '@mui/material/Grid'; import HTMLItem from './HTMLItem'; - +import { RootState } from '../../redux/store'; import { useSelector, useDispatch } from 'react-redux'; import { deleteElement } from '../../redux/reducers/slice/appStateSlice'; @@ -19,27 +19,34 @@ Hook state: */ // Extracted the drag and drop functionality from HTMLPanel to make a modular component that can hang wherever the future designers may choose. const DragDropPanel = (props): JSX.Element => { - const isDarkMode = useSelector(store => store.darkMode.isDarkMode); -const dispatch = useDispatch(); -const { state, contextParam } = useSelector((store) => ({ - state: store.appState, - contextParam: store.contextSlice, -})); + const isDarkMode = useSelector( + (store: RootState) => store.darkMode.isDarkMode + ); + const dispatch = useDispatch(); + const { state, contextParam } = useSelector((store: RootState) => ({ + state: store.appState, + contextParam: store.contextSlice + })); const handleDelete = (id: number): void => { - dispatch(deleteElement({id:id, contextParam: contextParam})) - + dispatch(deleteElement({ id: id, contextParam: contextParam })); }; // filter out separator so that it will not appear on the html panel - const htmlTypesToRender = state.HTMLTypes.filter(type => type.name !== 'separator'); + const htmlTypesToRender = state.HTMLTypes.filter( + (type) => type.name !== 'separator' + ); return (
    - -

    HTML ELEMENTS

    - {htmlTypesToRender.map(option => { - if (!['Switch', 'LinkTo', 'LinkHref', 'Image', 'Route'].includes(option.name)) { + +

    + HTML ELEMENTS +

    + {htmlTypesToRender.map((option) => { + if ( + !['Switch', 'LinkTo', 'LinkHref', 'Image', 'Route'].includes( + option.name + ) + ) { return ( ({ id={option.id} Icon={option.icon} handleDelete={handleDelete} - /> ); } - })} - {state.projectType === "Classic React" ?

    REACT ROUTER

    : null} - {htmlTypesToRender.map(option => { - if ((option.name === 'Switch' || option.name === 'LinkTo' || option.name === 'Route') && state.projectType === "Classic React") { + {state.projectType === 'Classic React' ? ( +

    + REACT ROUTER +

    + ) : null} + {htmlTypesToRender.map((option) => { + if ( + (option.name === 'Switch' || + option.name === 'LinkTo' || + option.name === 'Route') && + state.projectType === 'Classic React' + ) { return ( ({ id={option.id} Icon={option.icon} handleDelete={handleDelete} - /> ); } })} - {state.projectType === "Next.js" ?

    Next.js

    : null} - {htmlTypesToRender.map(option => { - if ((option.framework === 'nextjs') && state.projectType === "Next.js") { - return ( - - ); - } + {state.projectType === 'Next.js' ? ( +

    Next.js

    + ) : null} + {htmlTypesToRender.map((option) => { + if ( + option.framework === 'nextjs' && + state.projectType === 'Next.js' + ) { + return ( + + ); + } })}
    diff --git a/app/src/components/left/HTMLItem.tsx b/app/src/components/left/HTMLItem.tsx index aa0970cd2..fa7ba8dc5 100644 --- a/app/src/components/left/HTMLItem.tsx +++ b/app/src/components/left/HTMLItem.tsx @@ -8,6 +8,7 @@ import ListItem from '@mui/material/ListItem'; import ListItemText from '@mui/material/ListItemText'; import createModal from '../right/createModal'; import { useSelector } from 'react-redux'; +import { RootState } from '../../redux/store'; const useStyles = makeStyles({ HTMLPanelItem: { @@ -41,12 +42,12 @@ const HTMLItem : React.FC<{ id: number; Icon: any; handleDelete: (id: number) => void; -}> = ({ name, id, Icon, handleDelete }) => { +}> = ({ name, id, handleDelete }) => { const classes = useStyles(); const [modal, setModal] = useState(null); - const isDarkMode = useSelector(store => store.darkMode.isDarkMode); - const [{ isDragging }, drag] = useDrag({ + const isDarkMode = useSelector((store:RootState) => store.darkMode.isDarkMode); + const [{ isDragging }, drag] = useDrag({ // is dragging is never read, but if deleted adjustment in the ref are needed line 122/128 ref={drag} to {...drag} item: { type: ItemTypes.INSTANCE, newInstance: true, @@ -69,7 +70,6 @@ const HTMLItem : React.FC<{ handleDelete(deleteID)} style={{ border: '1px solid #3f51b5', @@ -85,7 +85,6 @@ const HTMLItem : React.FC<{ { const [name, setName] = useState(''); const [errorMsg, setErrorMsg] = useState(''); const [errorStatus, setErrorStatus] = useState(false); - const isDarkMode = useSelector(store => store.darkMode.isDarkMode); - const state = useSelector(store => store.appState); + const isDarkMode = useSelector((store:RootState) => store.darkMode.isDarkMode); + const state = useSelector((store:RootState) => store.appState); const dispatch = useDispatch(); let startingID = 0; - state.HTMLTypes.forEach(element => { + state.HTMLTypes.forEach((element) => { if (element.id >= startingID) startingID = element.id; }); startingID += 1; @@ -57,7 +59,7 @@ const HTMLPanel = (props): JSX.Element => { // checks to see if inputted comp name already exists let dupe = false; - checkList.forEach(HTMLTag => { + checkList.forEach((HTMLTag) => { if ( HTMLTag.name.toLowerCase() === inputName.toLowerCase() || HTMLTag.tag.toLowerCase() === inputName.toLowerCase() @@ -102,8 +104,8 @@ const HTMLPanel = (props): JSX.Element => { placeHolderLong: '', icon: null }; - - dispatch(addElement(newElement)) + + dispatch(addElement(newElement)); setCurrentID(currentID + 1); setTag(''); setName(''); @@ -115,18 +117,21 @@ const HTMLPanel = (props): JSX.Element => { return false; }; - const handleSubmit = e => { + const handleSubmit = (e) => { e.preventDefault(); - let letters = /[a-zA-Z]/; - if (!tag.charAt(0).match(letters) || !name.charAt(0).match(letters)) { + + if (tag.trim() === '' || name.trim() === '') { + triggerError('empty'); + return; + } else if ( + !tag.charAt(0).match(/[a-zA-Z]/) || + !name.charAt(0).match(/[a-zA-Z]/) + ) { triggerError('letters'); return; } else if (!alphanumeric(tag) || !alphanumeric(name)) { triggerError('symbolsDetected'); return; - } else if (tag.trim() === '' || name.trim() === '') { - triggerError('empty'); - return; } else if (checkNameDupe(tag) || checkNameDupe(name)) { triggerError('dupe'); return; @@ -139,66 +144,105 @@ const HTMLPanel = (props): JSX.Element => { }; const handleCreateElement = useCallback((e) => { - if(e.key === 'Enter' && e.target.tagName !== "TEXTAREA") { + if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA') { e.preventDefault(); document.getElementById('submitButton').click(); } }, []); - + useEffect(() => { document.addEventListener('keydown', handleCreateElement); return () => { - document.removeEventListener('keydown', handleCreateElement) - } + document.removeEventListener('keydown', handleCreateElement); + }; }, []); return ( -
    +
    - -

    New HTML Tag:

    - +

    + New HTML Tag:{' '} +

    + Tag: - - - {(!tag.charAt(0).match(/[A-Za-z]/) || !alphanumeric(tag) || tag.trim() === '' || checkNameDupe(tag)) - && - {errorMsg} - } - + + + {(!tag.charAt(0).match(/[A-Za-z]/) || + !alphanumeric(tag) || + tag.trim() === '' || + checkNameDupe(tag)) && ( + + {errorMsg} + + )} +

    - + Element Name: { } }} /> - {(!name.charAt(0).match(/[A-Za-z]/) || !alphanumeric(name) || name.trim() === '' || name.length > 10 || checkNameDupe(name)) - && - {errorMsg} - } + {(!name.charAt(0).match(/[A-Za-z]/) || + !alphanumeric(name) || + name.trim() === '' || + name.length > 10 || + checkNameDupe(name)) && ( + + {errorMsg} + + )}

    @@ -237,7 +297,7 @@ const useStyles = makeStyles({ backgroundColor: 'rgba(255,255,255,0.15)', margin: '0px 0px 0px 10px', width: '140px', - height: '30px', + height: '30px' }, inputWrapper: { textAlign: 'center', @@ -245,7 +305,7 @@ const useStyles = makeStyles({ alignItems: 'center', justifyContent: 'space-evenly', marginBottom: '15px', - width: '100%', + width: '100%' }, addComponentWrapper: { width: '100%', @@ -258,14 +318,12 @@ const useStyles = makeStyles({ textOverflow: 'ellipsis', backgroundColor: 'rgba(255,255,255,0.15)', margin: '0px 0px 0px 0px', - // width: '200px', - // height: '75px', alignSelf: 'center', border: '2px solid grey' }, inputLabel: { - fontSize: "1em", - marginLeft: "10px", + fontSize: '1em', + marginLeft: '10px' }, addElementButton: { backgroundColor: 'transparent', @@ -282,21 +340,21 @@ const useStyles = makeStyles({ lightThemeFontColor: { color: '#155084', '& .MuiInputBase-root': { - color: 'rgba (0, 0, 0, 0.54)', + color: 'rgba (0, 0, 0, 0.54)' } }, darkThemeFontColor: { color: '#ffffff', '& .MuiInputBase-root': { - color: '#fff', + color: '#fff' } }, errorMessage: { display: 'flex', alignSelf: 'center', - fontSize:"11px", - marginTop: "10px", - width: "150px", + fontSize: '11px', + marginTop: '10px', + width: '150px' }, errorMessageLight: { color: '#6B6B6B' @@ -306,7 +364,4 @@ const useStyles = makeStyles({ } }); - - - export default HTMLPanel; diff --git a/app/src/components/login/FBPassWord.tsx b/app/src/components/login/FBPassWord.tsx index 5e13bdc87..2672f584c 100644 --- a/app/src/components/login/FBPassWord.tsx +++ b/app/src/components/login/FBPassWord.tsx @@ -1,4 +1,4 @@ -import React, { useState} from 'react'; +import React, { useState, MouseEvent} from 'react'; import { LoginInt } from '../../interfaces/Interfaces'; import { Link as RouteLink, diff --git a/app/src/components/login/SignIn.tsx b/app/src/components/login/SignIn.tsx index 1c77abc47..597b43801 100644 --- a/app/src/components/login/SignIn.tsx +++ b/app/src/components/login/SignIn.tsx @@ -27,8 +27,9 @@ import { } from '../../../../app/src/public/styles/theme'; import Brightness3Icon from '@mui/icons-material/Brightness3'; import Brightness5Icon from '@mui/icons-material/Brightness5'; +import { RootState } from '../../redux/store'; -import config from '../../../../config'; +import config from '../../../../config.js'; const { API_BASE_URL } = config; declare module '@mui/styles/defaultTheme' { @@ -48,21 +49,17 @@ function Copyright() { const useStyles = makeStyles((theme) => ({ paper: { - // marginTop: theme.spacing(8), display: 'flex', flexDirection: 'column', alignItems: 'center' }, avatar: { - // margin: theme.spacing(1), backgroundColor: '#3EC1AC' }, form: { width: '100%' // Fix IE 11 issue. - // marginTop: theme.spacing(1) }, submit: { - // margin: theme.spacing(1, 0, 1), cursor: 'pointer' }, root: { @@ -79,7 +76,9 @@ const SignIn: React.FC = (props) => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); - const isDarkMode = useSelector((store) => store.darkMode.isDarkMode); + const isDarkMode = useSelector( + (store: RootState) => store.darkMode.isDarkMode + ); const [invalidUserMsg, setInvalidUserMsg] = useState(''); const [invalidPassMsg, setInvalidPassMsg] = useState(''); @@ -180,25 +179,26 @@ const SignIn: React.FC = (props) => { // 8080 for container 5656 for dev window.location.assign(`${API_BASE_URL}/auth/github`); }; - const responseFacebook = (response) => { - if (response.accessToken) { - newUserIsCreated(response.email, response.email, randomPassword()).then( - (userCreated) => { - if (userCreated === 'Success') { - props.history.push('/'); - } else { - sessionIsCreated(response.email, randomPassword(), true).then( - (loginStatus) => { - if (loginStatus === 'Success') { - props.history.push('/'); - } - } - ); - } - } - ); - } - }; + + // const responseFacebook = (response) => { + // if (response.accessToken) { + // newUserIsCreated(response.email, response.email, randomPassword()).then( + // (userCreated) => { + // if (userCreated === 'Success') { + // props.history.push('/'); + // } else { + // sessionIsCreated(response.email, randomPassword(), true).then( + // (loginStatus) => { + // if (loginStatus === 'Success') { + // props.history.push('/'); + // } + // } + // ); + // } + // } + // ); + // } + // }; // NEW DARK MODE const handleDarkModeToggle = () => { @@ -222,7 +222,6 @@ const SignIn: React.FC = (props) => { right: 20, position: 'absolute' }} - // variant="contained" endIcon={!isDarkMode ? : } onClick={handleDarkModeToggle} > @@ -249,6 +248,7 @@ const SignIn: React.FC = (props) => { onChange={handleChange} helperText={invalidUserMsg} error={invalidUser} + data-testid="username-input" /> = (props) => { onChange={handleChange} helperText={invalidPassMsg} error={invalidPass} + data-testid="password-input" />