From 02da46ab06f1d39f1b406639b912906d9b622e3d Mon Sep 17 00:00:00 2001 From: Linda Paiste Date: Wed, 26 Jul 2023 16:54:51 -0500 Subject: [PATCH 01/17] Improve Redux test utilities. --- client/test-utils.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/client/test-utils.js b/client/test-utils.js index ffabcb0df2..923cd75a4b 100644 --- a/client/test-utils.js +++ b/client/test-utils.js @@ -14,15 +14,14 @@ import { render } from '@testing-library/react'; import React from 'react'; import PropTypes from 'prop-types'; -import { createStore } from 'redux'; import { Provider } from 'react-redux'; import { I18nextProvider } from 'react-i18next'; import { ThemeProvider as StyledThemeProvider } from 'styled-components'; import i18n from './i18n-test'; -import rootReducer from './reducers'; import ThemeProvider from './modules/App/components/ThemeProvider'; +import configureStore from './store'; import theme, { Theme } from './theme'; // re-export everything @@ -42,11 +41,7 @@ Providers.propTypes = { function reduxRender( ui, - { - initialState, - store = createStore(rootReducer, initialState), - ...renderOptions - } = {} + { initialState, store = configureStore(initialState), ...renderOptions } = {} ) { function Wrapper({ children }) { return ( @@ -62,7 +57,7 @@ function reduxRender( children: PropTypes.element.isRequired }; - return render(ui, { wrapper: Wrapper, ...renderOptions }); + return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) }; } const customRender = (ui, options) => From 319edb17e1028bd7e0ff82a98cea5c9e45ce51ad Mon Sep 17 00:00:00 2001 From: Linda Paiste Date: Wed, 26 Jul 2023 16:54:51 -0500 Subject: [PATCH 02/17] Improve Redux test utilities. --- client/test-utils.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/client/test-utils.js b/client/test-utils.js index ffabcb0df2..923cd75a4b 100644 --- a/client/test-utils.js +++ b/client/test-utils.js @@ -14,15 +14,14 @@ import { render } from '@testing-library/react'; import React from 'react'; import PropTypes from 'prop-types'; -import { createStore } from 'redux'; import { Provider } from 'react-redux'; import { I18nextProvider } from 'react-i18next'; import { ThemeProvider as StyledThemeProvider } from 'styled-components'; import i18n from './i18n-test'; -import rootReducer from './reducers'; import ThemeProvider from './modules/App/components/ThemeProvider'; +import configureStore from './store'; import theme, { Theme } from './theme'; // re-export everything @@ -42,11 +41,7 @@ Providers.propTypes = { function reduxRender( ui, - { - initialState, - store = createStore(rootReducer, initialState), - ...renderOptions - } = {} + { initialState, store = configureStore(initialState), ...renderOptions } = {} ) { function Wrapper({ children }) { return ( @@ -62,7 +57,7 @@ function reduxRender( children: PropTypes.element.isRequired }; - return render(ui, { wrapper: Wrapper, ...renderOptions }); + return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) }; } const customRender = (ui, options) => From d26d062d028ab97ca5a37ebf2b6805af26c3bcb1 Mon Sep 17 00:00:00 2001 From: Linda Paiste Date: Sat, 29 Jul 2023 14:32:36 -0500 Subject: [PATCH 03/17] Convert Preferences to a function component and connect to Redux. --- .../Preferences/Preferences.unit.test.jsx | 204 ++-- .../IDE/components/Preferences/index.jsx | 898 ++++++++---------- client/modules/IDE/pages/IDEView.jsx | 35 +- 3 files changed, 472 insertions(+), 665 deletions(-) diff --git a/client/modules/IDE/components/Preferences/Preferences.unit.test.jsx b/client/modules/IDE/components/Preferences/Preferences.unit.test.jsx index fe414ffb58..644657eb83 100644 --- a/client/modules/IDE/components/Preferences/Preferences.unit.test.jsx +++ b/client/modules/IDE/components/Preferences/Preferences.unit.test.jsx @@ -1,63 +1,23 @@ import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { fireEvent, render, screen } from '../../../../test-utils'; +import { act, fireEvent, reduxRender, screen } from '../../../../test-utils'; import Preferences from './index'; - -/* props to pass in: - * - this.props.fontSize : number - * - this.props.autosave : bool - * - this.props.autocloseBracketsQuotes : bool - * - this.props.autocompleteHinter : bool - * - this.props.linewrap : bool - * - this.props.lineNumbers : bool - * - this.props.theme : string - * - this.props.lintWarning : bool - * - this.props.textOutput : bool - * - this.props.gridOutput : bool - * - this.props.soundOutput : bool - * - t from internationalization - * - * - this.props.setFontSize(fontsize : number) - * - this.props.setAutosave(value : bool) - * - this.props.setAutocloseBracketsQuotes(value: bool) - * - this.props.setLinewrap(value : bool) - * - this.props.setLineNumbers(value : bool) - * - this.props.setTheme(color : string) -> can be {"light", "dark", "contrast"} - * - this.props.setLintWarning(value : bool) - * - this.props.setTextOutput(value : bool) - * - this.props.setGridOutput(value : bool) - * - this.props.setSoundOutput(value : bool) - * - - */ +import * as PreferencesActions from '../../actions/preferences'; describe('', () => { - let props = { - t: jest.fn(), - fontSize: 12, - autosave: false, - autocloseBracketsQuotes: false, - autocompleteHinter: false, - linewrap: false, - lineNumbers: false, - theme: 'contrast', - lintWarning: false, - textOutput: false, - gridOutput: false, - soundOutput: false, - setFontSize: jest.fn(), - setAutosave: jest.fn(), - setAutocloseBracketsQuotes: jest.fn(), - setAutocompleteHinter: jest.fn(), - setLinewrap: jest.fn(), - setLineNumbers: jest.fn(), - setTheme: jest.fn(), - setLintWarning: jest.fn(), - setTextOutput: jest.fn(), - setGridOutput: jest.fn(), - setSoundOutput: jest.fn() - }; - - const subject = () => render(); + // For backwards compatibility, spy on each action creator to see when it was dispatched. + const props = Object.fromEntries( + Object.keys(PreferencesActions).map((name) => { + const spied = jest.spyOn(PreferencesActions, name); + return [name, spied]; + }) + ); + + const subject = (initialPreferences = {}) => + reduxRender(, { + initialState: { + preferences: initialPreferences + } + }); afterEach(() => { jest.clearAllMocks(); @@ -67,7 +27,7 @@ describe('', () => { it('font size increase button says increase', () => { // render the component act(() => { - subject(); + subject({ fontSize: 12 }); }); // get ahold of the button for increasing text size @@ -82,7 +42,7 @@ describe('', () => { it('increase font size by 2 when clicking plus button', () => { // render the component with font size set to 12 act(() => { - subject(); + subject({ fontSize: 12 }); }); // get ahold of the button for increasing text size @@ -103,7 +63,7 @@ describe('', () => { it('font size decrease button says decrease', () => { // render the component with font size set to 12 act(() => { - subject(); + subject({ fontSize: 12 }); }); // get ahold of the button for decreasing font size @@ -118,7 +78,7 @@ describe('', () => { it('decrease font size by 2 when clicking minus button', () => { // render the component with font size set to 12 act(() => { - subject(); + subject({ fontSize: 12 }); }); // get ahold of the button for decreasing text size @@ -139,7 +99,7 @@ describe('', () => { it('font text field changes on manual text input', () => { // render the component with font size set to 12 act(() => { - subject(); + subject({ fontSize: 12 }); }); // get ahold of the text field @@ -167,7 +127,7 @@ describe('', () => { it('font size CAN NOT go over 36', () => { // render the component act(() => { - subject(); + subject({ fontSize: 12 }); }); // get ahold of the text field @@ -194,7 +154,7 @@ describe('', () => { it('font size CAN NOT go under 8', () => { // render the component act(() => { - subject(); + subject({ fontSize: 12 }); }); // get ahold of the text field @@ -223,7 +183,7 @@ describe('', () => { it('font size input field does NOT take non-integers', () => { // render the component act(() => { - subject(); + subject({ fontSize: 12 }); }); // get ahold of the text field @@ -253,7 +213,7 @@ describe('', () => { it('font size input field does NOT take "-"', () => { // render the component act(() => { - subject(); + subject({ fontSize: 12 }); }); // get ahold of the text field @@ -311,13 +271,9 @@ describe('', () => { describe('testing theme switching', () => { describe('dark mode', () => { - beforeAll(() => { - props.theme = 'dark'; - }); - it('switch to light', () => { act(() => { - subject(); + subject({ theme: 'dark' }); }); const themeRadioCurrent = screen.getByRole('radio', { @@ -338,13 +294,9 @@ describe('', () => { }); describe('light mode', () => { - beforeAll(() => { - props.theme = 'light'; - }); - it('switch to dark', () => { act(() => { - subject(); + subject({ theme: 'light' }); }); const themeRadioCurrent = screen.getByRole('radio', { @@ -365,7 +317,7 @@ describe('', () => { it('switch to contrast', () => { act(() => { - subject(); + subject({ theme: 'light' }); }); const themeRadioCurrent = screen.getByRole('radio', { name: /light theme on/i @@ -388,7 +340,7 @@ describe('', () => { describe('testing toggle UI elements on starting tab', () => { it('autosave toggle, starting at false', () => { act(() => { - subject(); + subject({ autosave: false }); }); // get ahold of the radio buttons for toggling autosave @@ -410,7 +362,7 @@ describe('', () => { it('autocloseBracketsQuotes toggle, starting at false', () => { // render the component with autocloseBracketsQuotes prop set to false act(() => { - subject(); + subject({ autocloseBracketsQuotes: false }); }); // get ahold of the radio buttons for toggling autocloseBracketsQuotes @@ -432,7 +384,7 @@ describe('', () => { it('autocompleteHinter toggle, starting at false', () => { // render the component with autocompleteHinter prop set to false act(() => { - subject(); + subject({ autocompleteHinter: false }); }); // get ahold of the radio buttons for toggling autocompleteHinter @@ -452,14 +404,10 @@ describe('', () => { }); describe('start autosave value at true', () => { - beforeAll(() => { - props.autosave = true; - }); - it('autosave toggle, starting at true', () => { // render the component with autosave prop set to true act(() => { - subject(); + subject({ autosave: true }); }); // get ahold of the radio buttons for toggling autosave @@ -480,13 +428,9 @@ describe('', () => { }); describe('start autoclose brackets value at true', () => { - beforeAll(() => { - props.autocloseBracketsQuotes = true; - }); - it('autocloseBracketsQuotes toggle, starting at true', () => { act(() => { - subject(); + subject({ autocloseBracketsQuotes: true }); }); // get ahold of the radio buttons for toggling autocloseBracketsQuotes @@ -507,14 +451,10 @@ describe('', () => { }); describe('start autocomplete hinter value at true', () => { - beforeAll(() => { - props.autocompleteHinter = true; - }); - it('autocompleteHinter toggle, starting at true', () => { // render the component with autocompleteHinter prop set to true act(() => { - subject(); + subject({ autocompleteHinter: true }); }); // get ahold of the radio buttons for toggling autocompleteHinter @@ -535,14 +475,10 @@ describe('', () => { }); describe('start linewrap at false', () => { - beforeAll(() => { - props.linewrap = false; - }); - it('linewrap toggle, starting at false', () => { // render the component with linewrap prop set to false act(() => { - subject(); + subject({ linewrap: false }); }); // get ahold of the radio buttons for toggling linewrap @@ -563,14 +499,10 @@ describe('', () => { }); describe('start linewrap at true', () => { - beforeAll(() => { - props.linewrap = true; - }); - it('linewrap toggle, starting at true', () => { // render the component with linewrap prop set to false act(() => { - subject(); + subject({ linewrap: true }); }); // get ahold of the radio buttons for toggling linewrap @@ -595,7 +527,7 @@ describe('', () => { it('can toggle sucessfully', () => { // render the component with lineNumbers prop set to false act(() => { - subject(); + subject({ lineNumbers: false }); }); // switch to accessibility @@ -626,14 +558,10 @@ describe('', () => { describe('testing toggle UI elements on accessibility tab', () => { describe('starting linenumbers at false', () => { - beforeAll(() => { - props.lineNumbers = false; - }); - it('lineNumbers toggle, starting at false', () => { // render the component with lineNumbers prop set to false act(() => { - subject(); + subject({ lineNumbers: false }); }); // switch tabs @@ -661,14 +589,10 @@ describe('', () => { }); describe('starting linenumbers at true', () => { - beforeAll(() => { - props.lineNumbers = true; - }); - it('lineNumbers toggle, starting at true', () => { // render the component with lineNumbers prop set to false act(() => { - subject(); + subject({ lineNumbers: true }); }); // switch tabs @@ -696,14 +620,10 @@ describe('', () => { }); describe('starting lintWarning at false', () => { - beforeAll(() => { - props.lintWarning = false; - }); - it('lintWarning toggle, starting at false', () => { // render the component with lintWarning prop set to false act(() => { - subject(); + subject({ lintWarning: false }); }); // switch tabs @@ -731,14 +651,10 @@ describe('', () => { }); describe('starting lintWarning at true', () => { - beforeAll(() => { - props.lintWarning = true; - }); - it('lintWarning toggle, starting at true', () => { // render the component with lintWarning prop set to false act(() => { - subject(); + subject({ lintWarning: true }); }); // switch tabs @@ -766,15 +682,12 @@ describe('', () => { }); const testCheckbox = (arialabel, startState, setter) => { - props = { - ...props, - textOutput: startState && arialabel === 'text output on', - soundOutput: startState && arialabel === 'sound output on', - gridOutput: startState && arialabel === 'table output on' - }; - act(() => { - subject(); + subject({ + textOutput: startState && arialabel === 'text output on', + soundOutput: startState && arialabel === 'sound output on', + gridOutput: startState && arialabel === 'table output on' + }); }); // switch tabs @@ -817,17 +730,12 @@ describe('', () => { }); describe('multiple checkboxes can be selected', () => { - beforeAll(() => { - props = { - ...props, - textOutput: true, - gridOutput: true - }; - }); - it('multiple checkboxes can be selected', () => { act(() => { - subject(); + subject({ + textOutput: true, + gridOutput: true + }); }); // switch tabs @@ -850,16 +758,12 @@ describe('', () => { }); describe('none of the checkboxes can be selected', () => { - beforeAll(() => { - props = { - ...props, - textOutput: false, - gridOutput: false - }; - }); it('none of the checkboxes can be selected', () => { act(() => { - subject(); + subject({ + textOutput: false, + gridOutput: false + }); }); // switch tabs diff --git a/client/modules/IDE/components/Preferences/index.jsx b/client/modules/IDE/components/Preferences/index.jsx index 87d0a28b9d..fa5859400e 100644 --- a/client/modules/IDE/components/Preferences/index.jsx +++ b/client/modules/IDE/components/Preferences/index.jsx @@ -1,46 +1,61 @@ -import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useRef, useState } from 'react'; import { Helmet } from 'react-helmet'; +import { useDispatch, useSelector } from 'react-redux'; import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; -import { withTranslation } from 'react-i18next'; -// import { bindActionCreators } from 'redux'; -// import { connect } from 'react-redux'; -// import * as PreferencesActions from '../actions/preferences'; - +import { useTranslation } from 'react-i18next'; import PlusIcon from '../../../../images/plus.svg'; import MinusIcon from '../../../../images/minus.svg'; import beepUrl from '../../../../sounds/audioAlert.mp3'; +import { + setTheme, + setAutosave, + setTextOutput, + setGridOutput, + setFontSize, + setLineNumbers, + setLintWarning, + setAutocloseBracketsQuotes, + setAutocompleteHinter, + setLinewrap +} from '../../actions/preferences'; -class Preferences extends React.Component { - constructor(props) { - super(props); - this.handleUpdateAutosave = this.handleUpdateAutosave.bind(this); - this.handleUpdateLinewrap = this.handleUpdateLinewrap.bind(this); - this.handleLintWarning = this.handleLintWarning.bind(this); - this.handleLineNumbers = this.handleLineNumbers.bind(this); - this.onFontInputChange = this.onFontInputChange.bind(this); - this.onFontInputSubmit = this.onFontInputSubmit.bind(this); - this.increaseFontSize = this.increaseFontSize.bind(this); - this.decreaseFontSize = this.decreaseFontSize.bind(this); - this.setFontSize = this.setFontSize.bind(this); +export default function Preferences() { + const { t } = useTranslation(); - this.state = { - fontSize: props.fontSize - }; - } + const dispatch = useDispatch(); + + const { + fontSize, + autosave, + linewrap, + lineNumbers, + lintWarning, + textOutput, + gridOutput, + theme, + autocloseBracketsQuotes, + autocompleteHinter + } = useSelector((state) => state.preferences); - onFontInputChange(event) { + const [state, setState] = useState({ fontSize }); + + function onFontInputChange(event) { const INTEGER_REGEX = /^[0-9\b]+$/; if (event.target.value === '' || INTEGER_REGEX.test(event.target.value)) { - this.setState({ + setState({ fontSize: event.target.value }); } } - onFontInputSubmit(event) { + function handleFontSize(value) { + setState({ fontSize: value }); + dispatch(setFontSize(value)); + } + + function onFontInputSubmit(event) { event.preventDefault(); - let value = parseInt(this.state.fontSize, 10); + let value = parseInt(state.fontSize, 10); if (Number.isNaN(value)) { value = 16; } @@ -50,476 +65,397 @@ class Preferences extends React.Component { if (value < 8) { value = 8; } - this.setFontSize(value); - } - - setFontSize(value) { - this.setState({ fontSize: value }); - this.props.setFontSize(value); - } - - decreaseFontSize() { - const newValue = Number(this.state.fontSize) - 2; - this.setFontSize(newValue); - } - - increaseFontSize() { - const newValue = Number(this.state.fontSize) + 2; - this.setFontSize(newValue); + handleFontSize(value); } - handleUpdateAutosave(event) { - const value = event.target.value === 'true'; - this.props.setAutosave(value); + function decreaseFontSize() { + const newValue = Number(state.fontSize) - 2; + handleFontSize(newValue); } - handleUpdateLinewrap(event) { - const value = event.target.value === 'true'; - this.props.setLinewrap(value); + function increaseFontSize() { + const newValue = Number(state.fontSize) + 2; + handleFontSize(newValue); } - handleLintWarning(event) { - const value = event.target.value === 'true'; - this.props.setLintWarning(value); - } - - handleLineNumbers(event) { - const value = event.target.value === 'true'; - this.props.setLineNumbers(value); - } + const fontSizeInputRef = useRef(null); - render() { - const beep = new Audio(beepUrl); - - return ( -
- - p5.js Web Editor | Preferences - - - -
- -

- {this.props.t('Preferences.GeneralSettings')} -

-
- -

- {this.props.t('Preferences.Accessibility')} -

-
-
-
- -
-

- {this.props.t('Preferences.Theme')} -

-
- this.props.setTheme('light')} - aria-label={this.props.t('Preferences.LightThemeARIA')} - name="light theme" - id="light-theme-on" - className="preference__radio-button" - value="light" - checked={this.props.theme === 'light'} - /> - - this.props.setTheme('dark')} - aria-label={this.props.t('Preferences.DarkThemeARIA')} - name="dark theme" - id="dark-theme-on" - className="preference__radio-button" - value="dark" - checked={this.props.theme === 'dark'} - /> - - this.props.setTheme('contrast')} - aria-label={this.props.t('Preferences.HighContrastThemeARIA')} - name="high contrast theme" - id="high-contrast-theme-on" - className="preference__radio-button" - value="contrast" - checked={this.props.theme === 'contrast'} - /> - -
-
-
-

- {this.props.t('Preferences.TextSize')} + return ( +
+ + p5.js Web Editor | Preferences + + + +
+ +

+ {t('Preferences.GeneralSettings')}

- -
- - { - this.fontSizeInput = element; - }} - onClick={() => { - this.fontSizeInput.select(); - }} - /> -
-
+
+ +
+

{t('Preferences.Theme')}

+
+ dispatch(setTheme('light'))} + aria-label={t('Preferences.LightThemeARIA')} + name="light theme" + id="light-theme-on" + className="preference__radio-button" + value="light" + checked={theme === 'light'} + /> + + dispatch(setTheme('dark'))} + aria-label={t('Preferences.DarkThemeARIA')} + name="dark theme" + id="dark-theme-on" + className="preference__radio-button" + value="dark" + checked={theme === 'dark'} + /> + + dispatch(setTheme('contrast'))} + aria-label={t('Preferences.HighContrastThemeARIA')} + name="high contrast theme" + id="high-contrast-theme-on" + className="preference__radio-button" + value="contrast" + checked={theme === 'contrast'} + /> +
-
-

- {this.props.t('Preferences.Autosave')} -

-
- this.props.setAutosave(true)} - aria-label={this.props.t('Preferences.AutosaveOnARIA')} - name="autosave" - id="autosave-on" - className="preference__radio-button" - value="On" - checked={this.props.autosave} - /> - - this.props.setAutosave(false)} - aria-label={this.props.t('Preferences.AutosaveOffARIA')} - name="autosave" - id="autosave-off" - className="preference__radio-button" - value="Off" - checked={!this.props.autosave} - /> - -
+
+
+

{t('Preferences.TextSize')}

+ +
+ + { + fontSizeInputRef.current?.select(); + }} + /> +
+ +
+
+

{t('Preferences.Autosave')}

+
+ dispatch(setAutosave(true))} + aria-label={t('Preferences.AutosaveOnARIA')} + name="autosave" + id="autosave-on" + className="preference__radio-button" + value="On" + checked={autosave} + /> + + dispatch(setAutosave(false))} + aria-label={t('Preferences.AutosaveOffARIA')} + name="autosave" + id="autosave-off" + className="preference__radio-button" + value="Off" + checked={!autosave} + /> +
-
-

- {this.props.t('Preferences.AutocloseBracketsQuotes')} -

-
- this.props.setAutocloseBracketsQuotes(true)} - aria-label={this.props.t( - 'Preferences.AutocloseBracketsQuotesOnARIA' - )} - name="autoclosebracketsquotes" - id="autoclosebracketsquotes-on" - className="preference__radio-button" - value="On" - checked={this.props.autocloseBracketsQuotes} - /> - - this.props.setAutocloseBracketsQuotes(false)} - aria-label={this.props.t( - 'Preferences.AutocloseBracketsQuotesOffARIA' - )} - name="autoclosebracketsquotes" - id="autoclosebracketsquotes-off" - className="preference__radio-button" - value="Off" - checked={!this.props.autocloseBracketsQuotes} - /> - -
+
+
+

+ {t('Preferences.AutocloseBracketsQuotes')} +

+
+ dispatch(setAutocloseBracketsQuotes(true))} + aria-label={t('Preferences.AutocloseBracketsQuotesOnARIA')} + name="autoclosebracketsquotes" + id="autoclosebracketsquotes-on" + className="preference__radio-button" + value="On" + checked={autocloseBracketsQuotes} + /> + + dispatch(setAutocloseBracketsQuotes(false))} + aria-label={t('Preferences.AutocloseBracketsQuotesOffARIA')} + name="autoclosebracketsquotes" + id="autoclosebracketsquotes-off" + className="preference__radio-button" + value="Off" + checked={!autocloseBracketsQuotes} + /> +
-
-

- {this.props.t('Preferences.AutocompleteHinter')} -

-
- this.props.setAutocompleteHinter(true)} - aria-label={this.props.t( - 'Preferences.AutocompleteHinterOnARIA' - )} - name="autocompletehinter" - id="autocompletehinter-on" - className="preference__radio-button" - value="On" - checked={this.props.autocompleteHinter} - /> - - this.props.setAutocompleteHinter(false)} - aria-label={this.props.t( - 'Preferences.AutocompleteHinterOffARIA' - )} - name="autocompletehinter" - id="autocompletehinter-off" - className="preference__radio-button" - value="Off" - checked={!this.props.autocompleteHinter} - /> - -
+
+
+

+ {t('Preferences.AutocompleteHinter')} +

+
+ dispatch(setAutocompleteHinter(true))} + aria-label={t('Preferences.AutocompleteHinterOnARIA')} + name="autocompletehinter" + id="autocompletehinter-on" + className="preference__radio-button" + value="On" + checked={autocompleteHinter} + /> + + dispatch(setAutocompleteHinter(false))} + aria-label={t('Preferences.AutocompleteHinterOffARIA')} + name="autocompletehinter" + id="autocompletehinter-off" + className="preference__radio-button" + value="Off" + checked={!autocompleteHinter} + /> +
-
-

- {this.props.t('Preferences.WordWrap')} -

-
- this.props.setLinewrap(true)} - aria-label={this.props.t('Preferences.LineWrapOnARIA')} - name="linewrap" - id="linewrap-on" - className="preference__radio-button" - value="On" - checked={this.props.linewrap} - /> - - this.props.setLinewrap(false)} - aria-label={this.props.t('Preferences.LineWrapOffARIA')} - name="linewrap" - id="linewrap-off" - className="preference__radio-button" - value="Off" - checked={!this.props.linewrap} - /> - -
+
+
+

{t('Preferences.WordWrap')}

+
+ dispatch(setLinewrap(true))} + aria-label={t('Preferences.LineWrapOnARIA')} + name="linewrap" + id="linewrap-on" + className="preference__radio-button" + value="On" + checked={linewrap} + /> + + dispatch(setLinewrap(false))} + aria-label={t('Preferences.LineWrapOffARIA')} + name="linewrap" + id="linewrap-off" + className="preference__radio-button" + value="Off" + checked={!linewrap} + /> +
- - -
-

- {this.props.t('Preferences.LineNumbers')} -

-
- this.props.setLineNumbers(true)} - aria-label={this.props.t('Preferences.LineNumbersOnARIA')} - name="line numbers" - id="line-numbers-on" - className="preference__radio-button" - value="On" - checked={this.props.lineNumbers} - /> - - this.props.setLineNumbers(false)} - aria-label={this.props.t('Preferences.LineNumbersOffARIA')} - name="line numbers" - id="line-numbers-off" - className="preference__radio-button" - value="Off" - checked={!this.props.lineNumbers} - /> - -
+
+
+ +
+

+ {t('Preferences.LineNumbers')} +

+
+ dispatch(setLineNumbers(true))} + aria-label={t('Preferences.LineNumbersOnARIA')} + name="line numbers" + id="line-numbers-on" + className="preference__radio-button" + value="On" + checked={lineNumbers} + /> + + dispatch(setLineNumbers(false))} + aria-label={t('Preferences.LineNumbersOffARIA')} + name="line numbers" + id="line-numbers-off" + className="preference__radio-button" + value="Off" + checked={!lineNumbers} + /> +
-
-

- {this.props.t('Preferences.LintWarningSound')} -

-
- this.props.setLintWarning(true)} - aria-label={this.props.t('Preferences.LintWarningOnARIA')} - name="lint warning" - id="lint-warning-on" - className="preference__radio-button" - value="On" - checked={this.props.lintWarning} - /> - - this.props.setLintWarning(false)} - aria-label={this.props.t('Preferences.LintWarningOffARIA')} - name="lint warning" - id="lint-warning-off" - className="preference__radio-button" - value="Off" - checked={!this.props.lintWarning} - /> - - -
+
+
+

+ {t('Preferences.LintWarningSound')} +

+
+ dispatch(setLintWarning(true))} + aria-label={t('Preferences.LintWarningOnARIA')} + name="lint warning" + id="lint-warning-on" + className="preference__radio-button" + value="On" + checked={lintWarning} + /> + + dispatch(setLintWarning(false))} + aria-label={t('Preferences.LintWarningOffARIA')} + name="lint warning" + id="lint-warning-off" + className="preference__radio-button" + value="Off" + checked={!lintWarning} + /> + +
-
-

- {this.props.t('Preferences.AccessibleTextBasedCanvas')} -

-
- {this.props.t('Preferences.UsedScreenReader')} -
+
+
+

+ {t('Preferences.AccessibleTextBasedCanvas')} +

+
+ {t('Preferences.UsedScreenReader')} +
-
- { - this.props.setTextOutput(event.target.checked); - }} - aria-label={this.props.t('Preferences.TextOutputARIA')} - name="text output" - id="text-output-on" - value="On" - checked={this.props.textOutput} - /> - - { - this.props.setGridOutput(event.target.checked); - }} - aria-label={this.props.t('Preferences.TableOutputARIA')} - name="table output" - id="table-output-on" - value="On" - checked={this.props.gridOutput} - /> - -
+
+ { + dispatch(setTextOutput(event.target.checked)); + }} + aria-label={t('Preferences.TextOutputARIA')} + name="text output" + id="text-output-on" + value="On" + checked={textOutput} + /> + + { + dispatch(setGridOutput(event.target.checked)); + }} + aria-label={t('Preferences.TableOutputARIA')} + name="table output" + id="table-output-on" + value="On" + checked={gridOutput} + /> +
- - -
- ); - } +

+
+
+
+ ); } - -Preferences.propTypes = { - fontSize: PropTypes.number.isRequired, - lineNumbers: PropTypes.bool.isRequired, - setFontSize: PropTypes.func.isRequired, - autosave: PropTypes.bool.isRequired, - linewrap: PropTypes.bool.isRequired, - setLineNumbers: PropTypes.func.isRequired, - setAutosave: PropTypes.func.isRequired, - setLinewrap: PropTypes.func.isRequired, - textOutput: PropTypes.bool.isRequired, - gridOutput: PropTypes.bool.isRequired, - setTextOutput: PropTypes.func.isRequired, - setGridOutput: PropTypes.func.isRequired, - lintWarning: PropTypes.bool.isRequired, - setLintWarning: PropTypes.func.isRequired, - theme: PropTypes.string.isRequired, - setTheme: PropTypes.func.isRequired, - autocloseBracketsQuotes: PropTypes.bool.isRequired, - setAutocloseBracketsQuotes: PropTypes.func.isRequired, - autocompleteHinter: PropTypes.bool.isRequired, - setAutocompleteHinter: PropTypes.func.isRequired, - t: PropTypes.func.isRequired -}; - -export default withTranslation()(Preferences); diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index 685dc3fd5b..850a4a2204 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -269,30 +269,7 @@ class IDEView extends React.Component { ariaLabel={this.props.t('Preferences.Settings')} closeOverlay={this.props.closePreferences} > - + )}
@@ -501,15 +478,6 @@ IDEView.propTypes = { autocompleteHinter: PropTypes.bool.isRequired }).isRequired, closePreferences: PropTypes.func.isRequired, - setAutocloseBracketsQuotes: PropTypes.func.isRequired, - setAutocompleteHinter: PropTypes.func.isRequired, - setFontSize: PropTypes.func.isRequired, - setAutosave: PropTypes.func.isRequired, - setLineNumbers: PropTypes.func.isRequired, - setLinewrap: PropTypes.func.isRequired, - setLintWarning: PropTypes.func.isRequired, - setTextOutput: PropTypes.func.isRequired, - setGridOutput: PropTypes.func.isRequired, setAllAccessibleOutput: PropTypes.func.isRequired, selectedFile: PropTypes.shape({ id: PropTypes.string.isRequired, @@ -536,7 +504,6 @@ IDEView.propTypes = { setRouteLeaveHook: PropTypes.func }).isRequired, route: PropTypes.oneOfType([PropTypes.object, PropTypes.element]).isRequired, - setTheme: PropTypes.func.isRequired, setPreviousPath: PropTypes.func.isRequired, showErrorModal: PropTypes.func.isRequired, hideErrorModal: PropTypes.func.isRequired, From e76ba866bbcd6b091c6663131e2fd9039d0d22d6 Mon Sep 17 00:00:00 2001 From: Linda Paiste Date: Mon, 31 Jul 2023 15:41:40 -0500 Subject: [PATCH 04/17] Create selector functions for files. --- client/components/Nav.jsx | 3 ++- client/modules/IDE/components/Editor.jsx | 6 ++---- client/modules/IDE/components/Sidebar.jsx | 5 ++--- client/modules/IDE/pages/IDEView.jsx | 6 ++---- client/modules/IDE/pages/MobileIDEView.jsx | 6 ++---- client/modules/IDE/selectors/files.js | 15 +++++++++++++++ client/modules/Mobile/MobileSketchView.jsx | 8 ++------ 7 files changed, 27 insertions(+), 22 deletions(-) create mode 100644 client/modules/IDE/selectors/files.js diff --git a/client/components/Nav.jsx b/client/components/Nav.jsx index 6e6a350d5e..06f3d8497b 100644 --- a/client/components/Nav.jsx +++ b/client/components/Nav.jsx @@ -12,6 +12,7 @@ import { setAllAccessibleOutput, setLanguage } from '../modules/IDE/actions/preferences'; +import { selectRootFile } from '../modules/IDE/selectors/files'; import { logoutUser } from '../modules/User/actions'; import getConfig from '../utils/getConfig'; @@ -403,7 +404,7 @@ function mapStateToProps(state) { project: state.project, user: state.user, unsavedChanges: state.ide.unsavedChanges, - rootFile: state.files.filter((file) => file.name === 'root')[0], + rootFile: selectRootFile(state), language: state.preferences.language, isUserOwner: getIsUserOwner(state), editorLink: selectSketchPath(state) diff --git a/client/modules/IDE/components/Editor.jsx b/client/modules/IDE/components/Editor.jsx index e07c16b1a0..f5e963363a 100644 --- a/client/modules/IDE/components/Editor.jsx +++ b/client/modules/IDE/components/Editor.jsx @@ -44,6 +44,7 @@ import '../../../utils/htmlmixed'; import '../../../utils/p5-javascript'; import Timer from '../components/Timer'; import EditorAccessibility from '../components/EditorAccessibility'; +import { selectActiveFile } from '../selectors/files'; import AssetPreview from './AssetPreview'; import { metaKey } from '../../../utils/metaKey'; import './show-hint'; @@ -601,10 +602,7 @@ Editor.propTypes = { function mapStateToProps(state) { return { files: state.files, - file: - state.files.find((file) => file.isSelectedFile) || - state.files.find((file) => file.name === 'sketch.js') || - state.files.find((file) => file.name !== 'root'), + file: selectActiveFile(state), htmlFile: getHTMLFile(state.files), ide: state.ide, preferences: state.preferences, diff --git a/client/modules/IDE/components/Sidebar.jsx b/client/modules/IDE/components/Sidebar.jsx index 3e92fd8436..b28a2fba4d 100644 --- a/client/modules/IDE/components/Sidebar.jsx +++ b/client/modules/IDE/components/Sidebar.jsx @@ -9,6 +9,7 @@ import { openProjectOptions, openUploadFileModal } from '../actions/ide'; +import { selectRootFile } from '../selectors/files'; import { getAuthenticated, selectCanEditSketch } from '../selectors/users'; import ConnectedFileNode from './FileNode'; @@ -23,9 +24,7 @@ export default function SideBar() { const [isFocused, setIsFocused] = useState(false); - const files = useSelector((state) => state.files); - // TODO: use `selectRootFile` defined in another PR - const rootFile = files.filter((file) => file.name === 'root')[0]; + const rootFile = useSelector(selectRootFile); const projectOptionsVisible = useSelector( (state) => state.ide.projectOptionsVisible ); diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index 685dc3fd5b..f61c176630 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -33,6 +33,7 @@ import About from '../components/About'; import AddToCollectionList from '../components/AddToCollectionList'; import Feedback from '../components/Feedback'; import { CollectionSearchbar } from '../components/Searchbar'; +import { selectActiveFile } from '../selectors/files'; import { getIsUserOwner } from '../selectors/users'; import RootPage from '../../../components/RootPage'; @@ -549,10 +550,7 @@ IDEView.propTypes = { function mapStateToProps(state) { return { - selectedFile: - state.files.find((file) => file.isSelectedFile) || - state.files.find((file) => file.name === 'sketch.js') || - state.files.find((file) => file.name !== 'root'), + selectedFile: selectActiveFile(state), htmlFile: getHTMLFile(state.files), ide: state.ide, preferences: state.preferences, diff --git a/client/modules/IDE/pages/MobileIDEView.jsx b/client/modules/IDE/pages/MobileIDEView.jsx index 6442a0adba..1a7eb3600d 100644 --- a/client/modules/IDE/pages/MobileIDEView.jsx +++ b/client/modules/IDE/pages/MobileIDEView.jsx @@ -41,6 +41,7 @@ import { remSize } from '../../../theme'; import ActionStrip from '../../../components/mobile/ActionStrip'; import useAsModal from '../../../components/useAsModal'; import Dropdown from '../../../components/Dropdown'; +import { selectActiveFile } from '../selectors/files'; import { getIsUserOwner } from '../selectors/users'; import { @@ -475,10 +476,7 @@ MobileIDEView.propTypes = { function mapStateToProps(state) { return { - selectedFile: - state.files.find((file) => file.isSelectedFile) || - state.files.find((file) => file.name === 'sketch.js') || - state.files.find((file) => file.name !== 'root'), + selectedFile: selectActiveFile(state), ide: state.ide, files: state.files, unsavedChanges: state.ide.unsavedChanges, diff --git a/client/modules/IDE/selectors/files.js b/client/modules/IDE/selectors/files.js new file mode 100644 index 0000000000..e2044b6d89 --- /dev/null +++ b/client/modules/IDE/selectors/files.js @@ -0,0 +1,15 @@ +import { createSelector } from 'reselect'; + +const selectFiles = (state) => state.files; + +export const selectRootFile = createSelector(selectFiles, (files) => + files.find((file) => file.name === 'root') +); + +export const selectActiveFile = createSelector( + selectFiles, + (files) => + files.find((file) => file.isSelectedFile) || + files.find((file) => file.name === 'sketch.js') || + files.find((file) => file.name !== 'root') +); diff --git a/client/modules/Mobile/MobileSketchView.jsx b/client/modules/Mobile/MobileSketchView.jsx index dc69550f6f..3cbb8ebb78 100644 --- a/client/modules/Mobile/MobileSketchView.jsx +++ b/client/modules/Mobile/MobileSketchView.jsx @@ -16,6 +16,7 @@ import { getHTMLFile } from '../IDE/reducers/files'; import { ExitIcon } from '../../common/icons'; import Footer from '../../components/mobile/Footer'; +import { selectActiveFile } from '../IDE/selectors/files'; import Content from './MobileViewContent'; const MobileSketchView = () => { @@ -23,12 +24,7 @@ const MobileSketchView = () => { const htmlFile = useSelector((state) => getHTMLFile(state.files)); const projectName = useSelector((state) => state.project.name); - const selectedFile = useSelector( - (state) => - state.files.find((file) => file.isSelectedFile) || - state.files.find((file) => file.name === 'sketch.js') || - state.files.find((file) => file.name !== 'root') - ); + const selectedFile = useSelector(selectActiveFile); const { setTextOutput, From cb7afdbb2c1151ce44638a6f1ad367599416e2db Mon Sep 17 00:00:00 2001 From: dewanshDT Date: Fri, 4 Aug 2023 11:28:55 +0530 Subject: [PATCH 05/17] overlays-overflow-fix --- client/styles/components/_overlay.scss | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client/styles/components/_overlay.scss b/client/styles/components/_overlay.scss index 7351b92e70..fc59987ce4 100644 --- a/client/styles/components/_overlay.scss +++ b/client/styles/components/_overlay.scss @@ -23,15 +23,22 @@ flex-wrap: wrap; flex-flow: column; max-height: 80%; + max-width: 80%; position: relative; padding-bottom: #{25 / $base-font-size}rem; + + @media (max-width: 650px) { + max-width: 100%; + max-height: 100%; + height: 100vh; + width: 100vw; + } } .overlay__header { display: flex; justify-content: space-between; padding: #{20 / $base-font-size}rem; - flex: 1 0 auto; } .overlay__actions { @@ -55,4 +62,3 @@ .overlay--is-fixed-height .overlay__header { flex: 0; } - From f2b8b50f58c239c7518745f100d86aa3a121de00 Mon Sep 17 00:00:00 2001 From: dewanshDT Date: Sun, 6 Aug 2023 01:53:51 +0530 Subject: [PATCH 06/17] toolbar and the test file --- .../modules/IDE/components/Header/Toolbar.jsx | 378 ++++++++---------- .../components/Header/Toolbar.unit.test.jsx | 246 +++++++++--- client/modules/IDE/hooks/useSketchActions.js | 13 +- client/modules/IDE/selectors/project.js | 1 + 4 files changed, 360 insertions(+), 278 deletions(-) diff --git a/client/modules/IDE/components/Header/Toolbar.jsx b/client/modules/IDE/components/Header/Toolbar.jsx index 35470e97ad..0c1aa33a0c 100644 --- a/client/modules/IDE/components/Header/Toolbar.jsx +++ b/client/modules/IDE/components/Header/Toolbar.jsx @@ -1,253 +1,189 @@ +import React, { useRef } from 'react'; +import classNames from 'classnames'; import PropTypes from 'prop-types'; -import React from 'react'; -import { connect } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; import { Link } from 'react-router-dom'; -import classNames from 'classnames'; -import { withTranslation } from 'react-i18next'; -import * as IDEActions from '../../actions/ide'; -import * as preferenceActions from '../../actions/preferences'; -import * as projectActions from '../../actions/project'; +import { + hideEditProjectName, + showEditProjectName +} from '../../actions/project'; +import { + openPreferences, + startAccessibleSketch, + startSketch, + stopSketch +} from '../../actions/ide'; +import { + setAutorefresh, + setGridOutput, + setTextOutput +} from '../../actions/preferences'; +import { useSketchActions } from '../../hooks'; import PlayIcon from '../../../../images/play.svg'; import StopIcon from '../../../../images/stop.svg'; import PreferencesIcon from '../../../../images/preferences.svg'; import EditProjectNameIcon from '../../../../images/pencil.svg'; -class Toolbar extends React.Component { - constructor(props) { - super(props); - this.handleKeyPress = this.handleKeyPress.bind(this); - this.handleProjectNameChange = this.handleProjectNameChange.bind(this); - this.handleProjectNameClick = this.handleProjectNameClick.bind(this); - this.handleProjectNameSave = this.handleProjectNameSave.bind(this); +const Toolbar = (props) => { + const { isPlaying, infiniteLoop, preferencesIsVisible } = useSelector( + (state) => state.ide + ); + const project = useSelector((state) => state.project); + const { autorefresh } = useSelector((state) => state.preferences); + const dispatch = useDispatch(); - this.state = { - projectNameInputValue: props.project.name - }; - } + const { t } = useTranslation(); + const { changeSketchName, canEditProjectName } = useSketchActions(); + + const projectNameInputRef = useRef(); - handleKeyPress(event) { + function handleKeyPress(event) { if (event.key === 'Enter') { - this.props.hideEditProjectName(); - this.projectNameInput.blur(); + dispatch(hideEditProjectName()); + projectNameInputRef.current.blur(); } } - handleProjectNameChange(event) { - this.setState({ projectNameInputValue: event.target.value }); - } - - handleProjectNameClick() { - if (this.canEditProjectName) { - this.props.showEditProjectName(); + function handleProjectNameClick() { + if (canEditProjectName) { + dispatch(showEditProjectName()); setTimeout(() => { - this.projectNameInput?.focus(); + projectNameInputRef.current?.focus(); }, 140); } } - handleProjectNameSave() { - const newProjectName = this.state.projectNameInputValue.trim(); - if (newProjectName.length === 0) { - this.setState({ - projectNameInputValue: this.props.project.name - }); - } else { - this.props.setProjectName(newProjectName); - this.props.hideEditProjectName(); - if (this.props.project.id) { - this.props.saveProject(); - } + function handleProjectNameSave() { + const newName = projectNameInputRef.current?.value; + if (newName.length > 0) { + dispatch(hideEditProjectName()); + changeSketchName(newName); } } - canEditProjectName() { - return ( - (this.props.owner && - this.props.owner.username && - this.props.owner.username === this.props.currentUser) || - !this.props.owner || - !this.props.owner.username - ); - } - - render() { - const playButtonClass = classNames({ - 'toolbar__play-button': true, - 'toolbar__play-button--selected': this.props.isPlaying - }); - const stopButtonClass = classNames({ - 'toolbar__stop-button': true, - 'toolbar__stop-button--selected': !this.props.isPlaying - }); - const preferencesButtonClass = classNames({ - 'toolbar__preferences-button': true, - 'toolbar__preferences-button--selected': this.props.preferencesIsVisible - }); - const nameContainerClass = classNames({ - 'toolbar__project-name-container': true, - 'toolbar__project-name-container--editing': this.props.project - .isEditingName - }); - - const canEditProjectName = this.canEditProjectName(); - - return ( -
- - + + +
+ { + dispatch(setAutorefresh(event.target.checked)); }} - aria-label={this.props.t('Toolbar.PlayOnlyVisualSketchARIA')} - title={this.props.t('Toolbar.PlaySketchARIA')} - disabled={this.props.infiniteLoop} - > -
+
+ (event.target.value = project.name)} + onKeyPress={handleKeyPress} + /> + {(() => { + if (project.owner) { + return ( +

+ {t('Toolbar.By')}{' '} + + {project.owner.username} + +

+ ); + } + return null; + })()}
- ); - } -} + +
+ ); +}; Toolbar.propTypes = { - isPlaying: PropTypes.bool.isRequired, - preferencesIsVisible: PropTypes.bool.isRequired, - stopSketch: PropTypes.func.isRequired, - setProjectName: PropTypes.func.isRequired, - openPreferences: PropTypes.func.isRequired, - owner: PropTypes.shape({ - username: PropTypes.string - }), - project: PropTypes.shape({ - name: PropTypes.string.isRequired, - isEditingName: PropTypes.bool, - id: PropTypes.string - }).isRequired, - showEditProjectName: PropTypes.func.isRequired, - hideEditProjectName: PropTypes.func.isRequired, - infiniteLoop: PropTypes.bool.isRequired, - autorefresh: PropTypes.bool.isRequired, - setAutorefresh: PropTypes.func.isRequired, - setTextOutput: PropTypes.func.isRequired, - setGridOutput: PropTypes.func.isRequired, - startSketch: PropTypes.func.isRequired, - startAccessibleSketch: PropTypes.func.isRequired, - saveProject: PropTypes.func.isRequired, - currentUser: PropTypes.string, - t: PropTypes.func.isRequired, syncFileContent: PropTypes.func.isRequired }; -Toolbar.defaultProps = { - owner: undefined, - currentUser: undefined -}; - -function mapStateToProps(state) { - return { - autorefresh: state.preferences.autorefresh, - currentUser: state.user.username, - infiniteLoop: state.ide.infiniteLoop, - isPlaying: state.ide.isPlaying, - owner: state.project.owner, - preferencesIsVisible: state.ide.preferencesIsVisible, - project: state.project - }; -} - -const mapDispatchToProps = { - ...IDEActions, - ...preferenceActions, - ...projectActions -}; - -export const ToolbarComponent = withTranslation()(Toolbar); -export default connect(mapStateToProps, mapDispatchToProps)(ToolbarComponent); +export default Toolbar; diff --git a/client/modules/IDE/components/Header/Toolbar.unit.test.jsx b/client/modules/IDE/components/Header/Toolbar.unit.test.jsx index cd3365d62d..d464bd18d2 100644 --- a/client/modules/IDE/components/Header/Toolbar.unit.test.jsx +++ b/client/modules/IDE/components/Header/Toolbar.unit.test.jsx @@ -1,73 +1,227 @@ +// import { rest } from 'msw'; +// import { setupServer } from 'msw/node'; +// import React from 'react'; +// import lodash from 'lodash'; +// import { Provider } from 'react-redux'; // Import the Provider from react-redux +// import { createStore, applyMiddleware } from 'redux'; +// import thunk from 'redux-thunk'; + +// import { +// fireEvent, +// reduxRender, +// screen, +// waitFor +// } from '../../../../test-utils'; +// import { selectProjectName } from '../../selectors/project'; +// import ToolbarComponent from './Toolbar'; +// import rootReducer from '../../../../reducers'; + +// const server = setupServer( +// rest.put(`/projects/id`, (req, res, ctx) => res(ctx.json(req.body))) +// ); + +// beforeAll(() => server.listen()); +// afterEach(() => server.resetHandlers()); +// afterAll(() => server.close()); + +// // Set up the Redux store with the root reducer and thunk middleware +// const store = createStore(rootReducer, applyMiddleware(thunk)); + +// const renderComponent = (extraState = {}) => { +// const initialState = lodash.merge( +// { +// ide: { +// isPlaying: false +// }, +// user: { +// authenticated: true, +// username: 'me', +// id: 'userId' +// }, +// project: { +// name: 'testname', +// id: 'id', +// owner: { +// username: 'me', +// id: 'userId' +// } +// } +// }, +// extraState +// ); + +// const props = { +// syncFileContent: jest.fn() +// }; + +// return { +// ...props, +// ...reduxRender( +// +// +// , +// { initialState } +// ) +// }; +// }; + +// describe('', () => { +// it('sketch owner can switch to sketch name editing mode', async () => { +// renderComponent(); +// const sketchName = screen.getByLabelText('Edit sketch name'); + +// fireEvent.click(sketchName); + +// await waitFor(() => { +// expect(screen.getByLabelText('New sketch name')).toHaveFocus(); +// expect(screen.getByLabelText('New sketch name')).toBeEnabled(); +// }); +// }); + +// it("non-owner can't switch to sketch editing mode", async () => { +// renderComponent({ user: { username: 'not-me', id: 'not-me' } }); +// const sketchName = screen.getByLabelText('Edit sketch name'); + +// fireEvent.click(sketchName); + +// expect(sketchName).toBeDisabled(); +// await waitFor(() => +// expect(screen.getByLabelText('New sketch name')).toBeDisabled() +// ); +// }); + +// it("sketch owner can't change to empty name", async () => { +// renderComponent(); +// const sketchNameInput = screen.getByLabelText('New sketch name'); +// fireEvent.change(sketchNameInput, { target: { value: '' } }); +// fireEvent.blur(sketchNameInput); + +// await waitFor(() => +// expect(selectProjectName(store.getState())).toBe('testname') +// ); +// }); + +// it('sketch owner can change name', async () => { +// renderComponent(); +// const sketchNameInput = screen.getByLabelText('New sketch name'); +// fireEvent.change(sketchNameInput, { +// target: { value: 'my new sketch name' } +// }); +// fireEvent.blur(sketchNameInput); + +// await waitFor(() => +// expect(selectProjectName(store.getState())).toBe('my new sketch name') +// ); +// }); + +// it('sketch is stopped when stop button is clicked', async () => { +// renderComponent(); +// const stopButton = screen.getByLabelText('Stop sketch'); + +// fireEvent.click(stopButton); + +// await waitFor(() => expect(store.getState().ide.isPlaying).toBe(false)); +// }); + +// it('sketch is started when play button is clicked', async () => { +// renderComponent(); +// const playButton = screen.getByLabelText('Play only visual sketch'); +// fireEvent.click(playButton); + +// await waitFor(() => expect(store.getState().ide.isPlaying).toBe(true)); +// }); + +// it('sketch content is synched when play button is clicked', async () => { +// const props = renderComponent(); +// const playButton = screen.getByLabelText('Play only visual sketch'); +// fireEvent.click(playButton); + +// await waitFor(() => expect(props.syncFileContent).toHaveBeenCalled()); +// }); +// }); + +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; import React from 'react'; import lodash from 'lodash'; -import { fireEvent, render, screen, waitFor } from '../../../../test-utils'; -import { ToolbarComponent } from './Toolbar'; +import { + fireEvent, + reduxRender, + screen, + waitFor +} from '../../../../test-utils'; +import { selectProjectName } from '../../selectors/project'; +import ToolbarComponent from './Toolbar'; + +const server = setupServer( + rest.put(`/projects/id`, (req, res, ctx) => res(ctx.json(req.body))) +); -const renderComponent = (extraProps = {}) => { - const props = lodash.merge( +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); + +const renderComponent = (extraState = {}) => { + const initialState = lodash.merge( { - isPlaying: false, - preferencesIsVisible: false, - stopSketch: jest.fn(), - setProjectName: jest.fn(), - openPreferences: jest.fn(), - showEditProjectName: jest.fn(), - hideEditProjectName: jest.fn(), - infiniteLoop: false, - autorefresh: false, - setAutorefresh: jest.fn(), - setTextOutput: jest.fn(), - setGridOutput: jest.fn(), - startSketch: jest.fn(), - startAccessibleSketch: jest.fn(), - saveProject: jest.fn(), - syncFileContent: jest.fn(), - currentUser: 'me', - originalProjectName: 'testname', - - owner: { - username: 'me' + ide: { + isPlaying: false + }, + user: { + authenticated: true, + username: 'me', + id: 'userId' }, project: { name: 'testname', - isEditingName: false, - id: 'id' - }, - t: jest.fn() + id: 'id', + owner: { + username: 'me', + id: 'userId' + } + } }, - extraProps + extraState ); - render(); + const props = { + syncFileContent: jest.fn() + }; - return props; + return { + ...props, + ...reduxRender(, { initialState }) + }; }; describe('', () => { it('sketch owner can switch to sketch name editing mode', async () => { - const props = renderComponent(); + renderComponent(); const sketchName = screen.getByLabelText('Edit sketch name'); fireEvent.click(sketchName); - await waitFor(() => expect(props.showEditProjectName).toHaveBeenCalled()); + await waitFor(() => { + expect(screen.getByLabelText('New sketch name')).toHaveFocus(); + expect(screen.getByLabelText('New sketch name')).toBeEnabled(); + }); }); it("non-owner can't switch to sketch editing mode", async () => { - const props = renderComponent({ currentUser: 'not-me' }); + renderComponent({ user: { username: 'not-me', id: 'not-me' } }); const sketchName = screen.getByLabelText('Edit sketch name'); fireEvent.click(sketchName); expect(sketchName).toBeDisabled(); await waitFor(() => - expect(props.showEditProjectName).not.toHaveBeenCalled() + expect(screen.getByLabelText('New sketch name')).toBeDisabled() ); }); it('sketch owner can change name', async () => { - const props = renderComponent({ project: { isEditingName: true } }); + const { store } = renderComponent(); const sketchNameInput = screen.getByLabelText('New sketch name'); fireEvent.change(sketchNameInput, { @@ -76,38 +230,38 @@ describe('', () => { fireEvent.blur(sketchNameInput); await waitFor(() => - expect(props.setProjectName).toHaveBeenCalledWith('my new sketch name') + expect(selectProjectName(store.getState())).toBe('my new sketch name') ); - await waitFor(() => expect(props.saveProject).toHaveBeenCalled()); }); it("sketch owner can't change to empty name", async () => { - const props = renderComponent({ project: { isEditingName: true } }); + const { store } = renderComponent(); const sketchNameInput = screen.getByLabelText('New sketch name'); fireEvent.change(sketchNameInput, { target: { value: '' } }); fireEvent.blur(sketchNameInput); - await waitFor(() => expect(props.setProjectName).not.toHaveBeenCalled()); - await waitFor(() => expect(props.saveProject).not.toHaveBeenCalled()); + await waitFor(() => + expect(selectProjectName(store.getState())).toBe('testname') + ); }); it('sketch is stopped when stop button is clicked', async () => { - const props = renderComponent({ isPlaying: true }); + const { store } = renderComponent({ ide: { isPlaying: true } }); const stopButton = screen.getByLabelText('Stop sketch'); fireEvent.click(stopButton); - await waitFor(() => expect(props.stopSketch).toHaveBeenCalled()); + await waitFor(() => expect(store.getState().ide.isPlaying).toBe(false)); }); it('sketch is started when play button is clicked', async () => { - const props = renderComponent(); + const { store } = renderComponent(); const playButton = screen.getByLabelText('Play only visual sketch'); fireEvent.click(playButton); - await waitFor(() => expect(props.startSketch).toHaveBeenCalled()); + await waitFor(() => expect(store.getState().ide.isPlaying).toBe(true)); }); it('sketch content is synched when play button is clicked', async () => { diff --git a/client/modules/IDE/hooks/useSketchActions.js b/client/modules/IDE/hooks/useSketchActions.js index c03abb2665..10f548b1a4 100644 --- a/client/modules/IDE/hooks/useSketchActions.js +++ b/client/modules/IDE/hooks/useSketchActions.js @@ -10,12 +10,13 @@ import { } from '../actions/project'; import { showToast } from '../actions/toast'; import { showErrorModal, showShareModal } from '../actions/ide'; +import { selectCanEditSketch } from '../selectors/users'; const useSketchActions = () => { const unsavedChanges = useSelector((state) => state.ide.unsavedChanges); const authenticated = useSelector((state) => state.user.authenticated); const project = useSelector((state) => state.project); - const currentUser = useSelector((state) => state.user.username); + const canEditProjectName = useSelector(selectCanEditSketch); const dispatch = useDispatch(); const { t } = useTranslation(); const params = useParams(); @@ -56,16 +57,6 @@ const useSketchActions = () => { } } - function canEditProjectName() { - return ( - (project.owner && - project.owner.username && - project.owner.username === currentUser) || - !project.owner || - !project.owner.username - ); - } - return { newSketch, saveSketch, diff --git a/client/modules/IDE/selectors/project.js b/client/modules/IDE/selectors/project.js index 26197f3b00..bb536e3242 100644 --- a/client/modules/IDE/selectors/project.js +++ b/client/modules/IDE/selectors/project.js @@ -2,6 +2,7 @@ import { createSelector } from 'reselect'; export const selectProjectOwner = (state) => state.project.owner; export const selectProjectId = (state) => state.project.id; +export const selectProjectName = (state) => state.project.name; export const selectSketchPath = createSelector( selectProjectOwner, From eb8c4b39bd2cf5099a2c86f1497c1edd4cec6e31 Mon Sep 17 00:00:00 2001 From: Linda Paiste Date: Wed, 26 Jul 2023 16:54:51 -0500 Subject: [PATCH 07/17] Improve Redux test utilities. --- client/test-utils.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/client/test-utils.js b/client/test-utils.js index fcd288974f..8265cdfbd4 100644 --- a/client/test-utils.js +++ b/client/test-utils.js @@ -14,7 +14,6 @@ import { render } from '@testing-library/react'; import React from 'react'; import PropTypes from 'prop-types'; -import { createStore } from 'redux'; import { Provider } from 'react-redux'; import { Router } from 'react-router-dom'; import { I18nextProvider } from 'react-i18next'; @@ -22,8 +21,8 @@ import { ThemeProvider as StyledThemeProvider } from 'styled-components'; import browserHistory from './browserHistory'; import i18n from './i18n-test'; -import rootReducer from './reducers'; import ThemeProvider from './modules/App/components/ThemeProvider'; +import configureStore from './store'; import theme, { Theme } from './theme'; // re-export everything @@ -45,11 +44,7 @@ Providers.propTypes = { function reduxRender( ui, - { - initialState, - store = createStore(rootReducer, initialState), - ...renderOptions - } = {} + { initialState, store = configureStore(initialState), ...renderOptions } = {} ) { function Wrapper({ children }) { return ( @@ -67,7 +62,7 @@ function reduxRender( children: PropTypes.element.isRequired }; - return render(ui, { wrapper: Wrapper, ...renderOptions }); + return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) }; } const customRender = (ui, options) => From e8aa35e84590e4d4ec5ddc95771e0d690e588d7f Mon Sep 17 00:00:00 2001 From: dewanshDT Date: Sun, 6 Aug 2023 03:32:39 +0530 Subject: [PATCH 08/17] added state to the name input --- client/modules/IDE/components/Header/Toolbar.jsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/client/modules/IDE/components/Header/Toolbar.jsx b/client/modules/IDE/components/Header/Toolbar.jsx index 0c1aa33a0c..d705ecb33e 100644 --- a/client/modules/IDE/components/Header/Toolbar.jsx +++ b/client/modules/IDE/components/Header/Toolbar.jsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React, { useRef, useState } from 'react'; import classNames from 'classnames'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; @@ -38,6 +38,7 @@ const Toolbar = (props) => { const { changeSketchName, canEditProjectName } = useSketchActions(); const projectNameInputRef = useRef(); + const [nameInputValue, setNameInputValue] = useState(project.name); function handleKeyPress(event) { if (event.key === 'Enter') { @@ -56,7 +57,7 @@ const Toolbar = (props) => { } function handleProjectNameSave() { - const newName = projectNameInputRef.current?.value; + const newName = nameInputValue; if (newName.length > 0) { dispatch(hideEditProjectName()); changeSketchName(newName); @@ -151,9 +152,11 @@ const Toolbar = (props) => { maxLength="128" className="toolbar__project-name-input" aria-label={t('Toolbar.NewSketchNameARIA')} + disabled={!canEditProjectName} ref={projectNameInputRef} + value={nameInputValue} + onChange={(e) => setNameInputValue(e.target.value)} onBlur={handleProjectNameSave} - onFocus={(event) => (event.target.value = project.name)} onKeyPress={handleKeyPress} /> {(() => { From 6889e7059e963503c1d25067646c956521bf86b6 Mon Sep 17 00:00:00 2001 From: dewanshDT Date: Sun, 6 Aug 2023 03:33:18 +0530 Subject: [PATCH 09/17] applied changes to the test file --- .../components/Header/Toolbar.unit.test.jsx | 144 +----------------- 1 file changed, 1 insertion(+), 143 deletions(-) diff --git a/client/modules/IDE/components/Header/Toolbar.unit.test.jsx b/client/modules/IDE/components/Header/Toolbar.unit.test.jsx index d464bd18d2..bcec6f2f6d 100644 --- a/client/modules/IDE/components/Header/Toolbar.unit.test.jsx +++ b/client/modules/IDE/components/Header/Toolbar.unit.test.jsx @@ -1,145 +1,3 @@ -// import { rest } from 'msw'; -// import { setupServer } from 'msw/node'; -// import React from 'react'; -// import lodash from 'lodash'; -// import { Provider } from 'react-redux'; // Import the Provider from react-redux -// import { createStore, applyMiddleware } from 'redux'; -// import thunk from 'redux-thunk'; - -// import { -// fireEvent, -// reduxRender, -// screen, -// waitFor -// } from '../../../../test-utils'; -// import { selectProjectName } from '../../selectors/project'; -// import ToolbarComponent from './Toolbar'; -// import rootReducer from '../../../../reducers'; - -// const server = setupServer( -// rest.put(`/projects/id`, (req, res, ctx) => res(ctx.json(req.body))) -// ); - -// beforeAll(() => server.listen()); -// afterEach(() => server.resetHandlers()); -// afterAll(() => server.close()); - -// // Set up the Redux store with the root reducer and thunk middleware -// const store = createStore(rootReducer, applyMiddleware(thunk)); - -// const renderComponent = (extraState = {}) => { -// const initialState = lodash.merge( -// { -// ide: { -// isPlaying: false -// }, -// user: { -// authenticated: true, -// username: 'me', -// id: 'userId' -// }, -// project: { -// name: 'testname', -// id: 'id', -// owner: { -// username: 'me', -// id: 'userId' -// } -// } -// }, -// extraState -// ); - -// const props = { -// syncFileContent: jest.fn() -// }; - -// return { -// ...props, -// ...reduxRender( -// -// -// , -// { initialState } -// ) -// }; -// }; - -// describe('', () => { -// it('sketch owner can switch to sketch name editing mode', async () => { -// renderComponent(); -// const sketchName = screen.getByLabelText('Edit sketch name'); - -// fireEvent.click(sketchName); - -// await waitFor(() => { -// expect(screen.getByLabelText('New sketch name')).toHaveFocus(); -// expect(screen.getByLabelText('New sketch name')).toBeEnabled(); -// }); -// }); - -// it("non-owner can't switch to sketch editing mode", async () => { -// renderComponent({ user: { username: 'not-me', id: 'not-me' } }); -// const sketchName = screen.getByLabelText('Edit sketch name'); - -// fireEvent.click(sketchName); - -// expect(sketchName).toBeDisabled(); -// await waitFor(() => -// expect(screen.getByLabelText('New sketch name')).toBeDisabled() -// ); -// }); - -// it("sketch owner can't change to empty name", async () => { -// renderComponent(); -// const sketchNameInput = screen.getByLabelText('New sketch name'); -// fireEvent.change(sketchNameInput, { target: { value: '' } }); -// fireEvent.blur(sketchNameInput); - -// await waitFor(() => -// expect(selectProjectName(store.getState())).toBe('testname') -// ); -// }); - -// it('sketch owner can change name', async () => { -// renderComponent(); -// const sketchNameInput = screen.getByLabelText('New sketch name'); -// fireEvent.change(sketchNameInput, { -// target: { value: 'my new sketch name' } -// }); -// fireEvent.blur(sketchNameInput); - -// await waitFor(() => -// expect(selectProjectName(store.getState())).toBe('my new sketch name') -// ); -// }); - -// it('sketch is stopped when stop button is clicked', async () => { -// renderComponent(); -// const stopButton = screen.getByLabelText('Stop sketch'); - -// fireEvent.click(stopButton); - -// await waitFor(() => expect(store.getState().ide.isPlaying).toBe(false)); -// }); - -// it('sketch is started when play button is clicked', async () => { -// renderComponent(); -// const playButton = screen.getByLabelText('Play only visual sketch'); -// fireEvent.click(playButton); - -// await waitFor(() => expect(store.getState().ide.isPlaying).toBe(true)); -// }); - -// it('sketch content is synched when play button is clicked', async () => { -// const props = renderComponent(); -// const playButton = screen.getByLabelText('Play only visual sketch'); -// fireEvent.click(playButton); - -// await waitFor(() => expect(props.syncFileContent).toHaveBeenCalled()); -// }); -// }); - import { rest } from 'msw'; import { setupServer } from 'msw/node'; import React from 'react'; @@ -216,7 +74,7 @@ describe('', () => { expect(sketchName).toBeDisabled(); await waitFor(() => - expect(screen.getByLabelText('New sketch name')).toBeDisabled() + expect(screen.getByLabelText('New sketch name').disabled).toBe(true) ); }); From 341eb3cf0c424a5f2689c69dce18dfcabf87a3c1 Mon Sep 17 00:00:00 2001 From: dewanshDT Date: Sun, 6 Aug 2023 22:41:37 +0530 Subject: [PATCH 10/17] suggested changes --- client/modules/IDE/components/Header/Toolbar.jsx | 5 ++--- client/modules/IDE/components/Header/Toolbar.unit.test.jsx | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/modules/IDE/components/Header/Toolbar.jsx b/client/modules/IDE/components/Header/Toolbar.jsx index d705ecb33e..46aeb9a652 100644 --- a/client/modules/IDE/components/Header/Toolbar.jsx +++ b/client/modules/IDE/components/Header/Toolbar.jsx @@ -31,7 +31,7 @@ const Toolbar = (props) => { (state) => state.ide ); const project = useSelector((state) => state.project); - const { autorefresh } = useSelector((state) => state.preferences); + const autorefresh = useSelector((state) => state.preferences.autorefresh); const dispatch = useDispatch(); const { t } = useTranslation(); @@ -43,7 +43,7 @@ const Toolbar = (props) => { function handleKeyPress(event) { if (event.key === 'Enter') { dispatch(hideEditProjectName()); - projectNameInputRef.current.blur(); + projectNameInputRef.current?.blur(); } } @@ -101,7 +101,6 @@ const Toolbar = (props) => { onClick={() => { props.syncFileContent(); dispatch(startSketch()); - console.log('play button pressed'); }} aria-label={t('Toolbar.PlayOnlyVisualSketchARIA')} title={t('Toolbar.PlaySketchARIA')} diff --git a/client/modules/IDE/components/Header/Toolbar.unit.test.jsx b/client/modules/IDE/components/Header/Toolbar.unit.test.jsx index bcec6f2f6d..82b4cb7f4c 100644 --- a/client/modules/IDE/components/Header/Toolbar.unit.test.jsx +++ b/client/modules/IDE/components/Header/Toolbar.unit.test.jsx @@ -74,7 +74,8 @@ describe('', () => { expect(sketchName).toBeDisabled(); await waitFor(() => - expect(screen.getByLabelText('New sketch name').disabled).toBe(true) + // expect(screen.getByLabelText('New sketch name').disabled).toBe(true) + expect(screen.getByLabelText('New sketch name')).toBeDisabled() ); }); From d452f201ee9c089574b7038e644cbd210f48ba76 Mon Sep 17 00:00:00 2001 From: Linda Paiste Date: Sun, 6 Aug 2023 19:27:09 -0500 Subject: [PATCH 11/17] Remove `act` around `subject`. --- .../Preferences/Preferences.unit.test.jsx | 127 +++++------------- 1 file changed, 36 insertions(+), 91 deletions(-) diff --git a/client/modules/IDE/components/Preferences/Preferences.unit.test.jsx b/client/modules/IDE/components/Preferences/Preferences.unit.test.jsx index 644657eb83..7f13eb4bad 100644 --- a/client/modules/IDE/components/Preferences/Preferences.unit.test.jsx +++ b/client/modules/IDE/components/Preferences/Preferences.unit.test.jsx @@ -26,9 +26,7 @@ describe('', () => { describe('font tests', () => { it('font size increase button says increase', () => { // render the component - act(() => { - subject({ fontSize: 12 }); - }); + subject({ fontSize: 12 }); // get ahold of the button for increasing text size const fontPlusButton = screen.getByRole('button', { @@ -41,9 +39,7 @@ describe('', () => { it('increase font size by 2 when clicking plus button', () => { // render the component with font size set to 12 - act(() => { - subject({ fontSize: 12 }); - }); + subject({ fontSize: 12 }); // get ahold of the button for increasing text size const fontPlusButton = screen.getByRole('button', { @@ -62,9 +58,7 @@ describe('', () => { it('font size decrease button says decrease', () => { // render the component with font size set to 12 - act(() => { - subject({ fontSize: 12 }); - }); + subject({ fontSize: 12 }); // get ahold of the button for decreasing font size const fontPlusButton = screen.getByRole('button', { @@ -77,9 +71,7 @@ describe('', () => { it('decrease font size by 2 when clicking minus button', () => { // render the component with font size set to 12 - act(() => { - subject({ fontSize: 12 }); - }); + subject({ fontSize: 12 }); // get ahold of the button for decreasing text size const fontMinusButton = screen.getByRole('button', { @@ -98,9 +90,7 @@ describe('', () => { it('font text field changes on manual text input', () => { // render the component with font size set to 12 - act(() => { - subject({ fontSize: 12 }); - }); + subject({ fontSize: 12 }); // get ahold of the text field const input = screen.getByRole('textbox', { name: /font size/i }); @@ -126,9 +116,7 @@ describe('', () => { it('font size CAN NOT go over 36', () => { // render the component - act(() => { - subject({ fontSize: 12 }); - }); + subject({ fontSize: 12 }); // get ahold of the text field const input = screen.getByRole('textbox', { name: /font size/i }); @@ -153,9 +141,7 @@ describe('', () => { it('font size CAN NOT go under 8', () => { // render the component - act(() => { - subject({ fontSize: 12 }); - }); + subject({ fontSize: 12 }); // get ahold of the text field const input = screen.getByRole('textbox', { name: /font size/i }); @@ -182,9 +168,7 @@ describe('', () => { // h and then i, but it tests the same idea it('font size input field does NOT take non-integers', () => { // render the component - act(() => { - subject({ fontSize: 12 }); - }); + subject({ fontSize: 12 }); // get ahold of the text field const input = screen.getByRole('textbox', { name: /font size/i }); @@ -212,9 +196,7 @@ describe('', () => { it('font size input field does NOT take "-"', () => { // render the component - act(() => { - subject({ fontSize: 12 }); - }); + subject({ fontSize: 12 }); // get ahold of the text field const input = screen.getByRole('textbox', { name: /font size/i }); @@ -272,9 +254,7 @@ describe('', () => { describe('testing theme switching', () => { describe('dark mode', () => { it('switch to light', () => { - act(() => { - subject({ theme: 'dark' }); - }); + subject({ theme: 'dark' }); const themeRadioCurrent = screen.getByRole('radio', { name: /dark theme on/i @@ -295,9 +275,7 @@ describe('', () => { describe('light mode', () => { it('switch to dark', () => { - act(() => { - subject({ theme: 'light' }); - }); + subject({ theme: 'light' }); const themeRadioCurrent = screen.getByRole('radio', { name: /light theme on/i @@ -316,9 +294,8 @@ describe('', () => { }); it('switch to contrast', () => { - act(() => { - subject({ theme: 'light' }); - }); + subject({ theme: 'light' }); + const themeRadioCurrent = screen.getByRole('radio', { name: /light theme on/i }); @@ -339,9 +316,7 @@ describe('', () => { describe('testing toggle UI elements on starting tab', () => { it('autosave toggle, starting at false', () => { - act(() => { - subject({ autosave: false }); - }); + subject({ autosave: false }); // get ahold of the radio buttons for toggling autosave const autosaveRadioFalse = screen.getByRole('radio', { @@ -361,9 +336,7 @@ describe('', () => { it('autocloseBracketsQuotes toggle, starting at false', () => { // render the component with autocloseBracketsQuotes prop set to false - act(() => { - subject({ autocloseBracketsQuotes: false }); - }); + subject({ autocloseBracketsQuotes: false }); // get ahold of the radio buttons for toggling autocloseBracketsQuotes const autocloseRadioFalse = screen.getByRole('radio', { @@ -383,9 +356,7 @@ describe('', () => { it('autocompleteHinter toggle, starting at false', () => { // render the component with autocompleteHinter prop set to false - act(() => { - subject({ autocompleteHinter: false }); - }); + subject({ autocompleteHinter: false }); // get ahold of the radio buttons for toggling autocompleteHinter const autocompleteRadioFalse = screen.getByRole('radio', { @@ -406,9 +377,7 @@ describe('', () => { describe('start autosave value at true', () => { it('autosave toggle, starting at true', () => { // render the component with autosave prop set to true - act(() => { - subject({ autosave: true }); - }); + subject({ autosave: true }); // get ahold of the radio buttons for toggling autosave const autosaveRadioFalse = screen.getByRole('radio', { @@ -429,9 +398,7 @@ describe('', () => { describe('start autoclose brackets value at true', () => { it('autocloseBracketsQuotes toggle, starting at true', () => { - act(() => { - subject({ autocloseBracketsQuotes: true }); - }); + subject({ autocloseBracketsQuotes: true }); // get ahold of the radio buttons for toggling autocloseBracketsQuotes const autocloseRadioFalse = screen.getByRole('radio', { @@ -453,9 +420,7 @@ describe('', () => { describe('start autocomplete hinter value at true', () => { it('autocompleteHinter toggle, starting at true', () => { // render the component with autocompleteHinter prop set to true - act(() => { - subject({ autocompleteHinter: true }); - }); + subject({ autocompleteHinter: true }); // get ahold of the radio buttons for toggling autocompleteHinter const autocompleteRadioFalse = screen.getByRole('radio', { @@ -477,9 +442,7 @@ describe('', () => { describe('start linewrap at false', () => { it('linewrap toggle, starting at false', () => { // render the component with linewrap prop set to false - act(() => { - subject({ linewrap: false }); - }); + subject({ linewrap: false }); // get ahold of the radio buttons for toggling linewrap const linewrapRadioFalse = screen.getByRole('radio', { @@ -501,9 +464,7 @@ describe('', () => { describe('start linewrap at true', () => { it('linewrap toggle, starting at true', () => { // render the component with linewrap prop set to false - act(() => { - subject({ linewrap: true }); - }); + subject({ linewrap: true }); // get ahold of the radio buttons for toggling linewrap const linewrapRadioFalse = screen.getByRole('radio', { @@ -526,9 +487,7 @@ describe('', () => { describe('can toggle between general settings and accessibility tabs successfully', () => { it('can toggle sucessfully', () => { // render the component with lineNumbers prop set to false - act(() => { - subject({ lineNumbers: false }); - }); + subject({ lineNumbers: false }); // switch to accessibility act(() => { @@ -560,9 +519,7 @@ describe('', () => { describe('starting linenumbers at false', () => { it('lineNumbers toggle, starting at false', () => { // render the component with lineNumbers prop set to false - act(() => { - subject({ lineNumbers: false }); - }); + subject({ lineNumbers: false }); // switch tabs act(() => { @@ -591,9 +548,7 @@ describe('', () => { describe('starting linenumbers at true', () => { it('lineNumbers toggle, starting at true', () => { // render the component with lineNumbers prop set to false - act(() => { - subject({ lineNumbers: true }); - }); + subject({ lineNumbers: true }); // switch tabs act(() => { @@ -622,9 +577,7 @@ describe('', () => { describe('starting lintWarning at false', () => { it('lintWarning toggle, starting at false', () => { // render the component with lintWarning prop set to false - act(() => { - subject({ lintWarning: false }); - }); + subject({ lintWarning: false }); // switch tabs act(() => { @@ -653,9 +606,7 @@ describe('', () => { describe('starting lintWarning at true', () => { it('lintWarning toggle, starting at true', () => { // render the component with lintWarning prop set to false - act(() => { - subject({ lintWarning: true }); - }); + subject({ lintWarning: true }); // switch tabs act(() => { @@ -682,12 +633,10 @@ describe('', () => { }); const testCheckbox = (arialabel, startState, setter) => { - act(() => { - subject({ - textOutput: startState && arialabel === 'text output on', - soundOutput: startState && arialabel === 'sound output on', - gridOutput: startState && arialabel === 'table output on' - }); + subject({ + textOutput: startState && arialabel === 'text output on', + soundOutput: startState && arialabel === 'sound output on', + gridOutput: startState && arialabel === 'table output on' }); // switch tabs @@ -731,11 +680,9 @@ describe('', () => { describe('multiple checkboxes can be selected', () => { it('multiple checkboxes can be selected', () => { - act(() => { - subject({ - textOutput: true, - gridOutput: true - }); + subject({ + textOutput: true, + gridOutput: true }); // switch tabs @@ -759,11 +706,9 @@ describe('', () => { describe('none of the checkboxes can be selected', () => { it('none of the checkboxes can be selected', () => { - act(() => { - subject({ - textOutput: false, - gridOutput: false - }); + subject({ + textOutput: false, + gridOutput: false }); // switch tabs From d99e25aea6f73d9c94035a6b9fdf74e44678c60c Mon Sep 17 00:00:00 2001 From: adityagarg06 Date: Tue, 8 Aug 2023 23:00:23 +0530 Subject: [PATCH 12/17] Added styled component in AddToCollectionList component --- .../IDE/components/AddToCollectionList.jsx | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/client/modules/IDE/components/AddToCollectionList.jsx b/client/modules/IDE/components/AddToCollectionList.jsx index 26addfaa34..f3885aa731 100644 --- a/client/modules/IDE/components/AddToCollectionList.jsx +++ b/client/modules/IDE/components/AddToCollectionList.jsx @@ -4,7 +4,7 @@ import { Helmet } from 'react-helmet'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { withTranslation } from 'react-i18next'; - +import styled from 'styled-components'; import * as ProjectActions from '../actions/project'; import * as ProjectsActions from '../actions/projects'; import * as CollectionsActions from '../actions/collections'; @@ -13,10 +13,26 @@ import * as SortingActions from '../actions/sorting'; import getSortedCollections from '../selectors/collections'; import Loader from '../../App/components/loader'; import QuickAddList from './QuickAddList'; +import { remSize } from '../../../theme'; const projectInCollection = (project, collection) => collection.items.find((item) => item.projectId === project.id) != null; +const CollectionAddSketchWrapper = styled.div` + &&& { + min-width: ${remSize(600)}; + overflow: auto; + } +`; + +const QuickAddWrapper = styled.div` + &&& { + min-width: ${remSize(600)}; + padding: ${remSize(24)}; + height: 100%; + } +`; + class CollectionList extends React.Component { constructor(props) { super(props); @@ -85,15 +101,15 @@ class CollectionList extends React.Component { } return ( -
-
+ + {this.getTitle()} {content} -
-
+ + ); } } From 8fc6dede90152f2df52fdbfdc2f937628123ede0 Mon Sep 17 00:00:00 2001 From: adityagarg06 Date: Wed, 9 Aug 2023 22:12:55 +0530 Subject: [PATCH 13/17] Added styled component to AddToCollectionSketchList and updated the css code for responsiveness --- .../IDE/components/AddToCollectionList.jsx | 17 ++++++-------- .../components/AddToCollectionSketchList.jsx | 23 +++++++++++++++---- client/styles/components/_collection.scss | 5 ---- client/styles/components/_quick-add.scss | 6 ----- 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/client/modules/IDE/components/AddToCollectionList.jsx b/client/modules/IDE/components/AddToCollectionList.jsx index f3885aa731..c8e2aa2e9b 100644 --- a/client/modules/IDE/components/AddToCollectionList.jsx +++ b/client/modules/IDE/components/AddToCollectionList.jsx @@ -19,18 +19,16 @@ const projectInCollection = (project, collection) => collection.items.find((item) => item.projectId === project.id) != null; const CollectionAddSketchWrapper = styled.div` - &&& { - min-width: ${remSize(600)}; - overflow: auto; - } + width: ${remSize(600)}; + max-width: 100%; + overflow: auto; `; const QuickAddWrapper = styled.div` - &&& { - min-width: ${remSize(600)}; - padding: ${remSize(24)}; - height: 100%; - } + width: ${remSize(600)}; + max-width: 100%; + padding: ${remSize(24)}; + height: 100%; `; class CollectionList extends React.Component { @@ -106,7 +104,6 @@ class CollectionList extends React.Component { {this.getTitle()} - {content} diff --git a/client/modules/IDE/components/AddToCollectionSketchList.jsx b/client/modules/IDE/components/AddToCollectionSketchList.jsx index e42aff8d60..de26dd7c43 100644 --- a/client/modules/IDE/components/AddToCollectionSketchList.jsx +++ b/client/modules/IDE/components/AddToCollectionSketchList.jsx @@ -5,6 +5,7 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { withTranslation } from 'react-i18next'; // import find from 'lodash/find'; +import styled from 'styled-components'; import * as ProjectsActions from '../actions/projects'; import * as CollectionsActions from '../actions/collections'; import * as ToastActions from '../actions/toast'; @@ -12,6 +13,20 @@ import * as SortingActions from '../actions/sorting'; import getSortedSketches from '../selectors/projects'; import Loader from '../../App/components/loader'; import QuickAddList from './QuickAddList'; +import { remSize } from '../../../theme'; + +const CollectionAddSketchWrapper = styled.div` + width: ${remSize(600)}; + max-width: 100%; + overflow: auto; +`; + +const QuickAddWrapper = styled.div` + width: ${remSize(600)}; + max-width: 100%; + padding: ${remSize(24)}; + height: 100%; +`; class SketchList extends React.Component { constructor(props) { @@ -81,14 +96,14 @@ class SketchList extends React.Component { } return ( -
-
+ + {this.getSketchesTitle()} {content} -
-
+ + ); } } diff --git a/client/styles/components/_collection.scss b/client/styles/components/_collection.scss index a0bac089b3..0c8707ecf6 100644 --- a/client/styles/components/_collection.scss +++ b/client/styles/components/_collection.scss @@ -84,11 +84,6 @@ width: 100%; } -.collection-add-sketch { - min-width: #{600 / $base-font-size}rem; - overflow: auto; -} - .collection-share { text-align: right; position: relative; diff --git a/client/styles/components/_quick-add.scss b/client/styles/components/_quick-add.scss index 35b6bcef8c..70a097fa1e 100644 --- a/client/styles/components/_quick-add.scss +++ b/client/styles/components/_quick-add.scss @@ -1,9 +1,3 @@ -.quick-add-wrapper { - min-width: #{600 / $base-font-size}rem; - padding: #{24 / $base-font-size}rem; - height: 100%; -} - .quick-add { width: auto; overflow-y: auto; From 5c1e45642aef8219053ed277f43ade9f73f9f797 Mon Sep 17 00:00:00 2001 From: adityagarg06 Date: Wed, 9 Aug 2023 22:25:24 +0530 Subject: [PATCH 14/17] Exported the styled components to the CollectionSketchList --- .../IDE/components/AddToCollectionList.jsx | 4 ++-- .../components/AddToCollectionSketchList.jsx | 19 ++++--------------- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/client/modules/IDE/components/AddToCollectionList.jsx b/client/modules/IDE/components/AddToCollectionList.jsx index c8e2aa2e9b..fc5c161fdc 100644 --- a/client/modules/IDE/components/AddToCollectionList.jsx +++ b/client/modules/IDE/components/AddToCollectionList.jsx @@ -18,13 +18,13 @@ import { remSize } from '../../../theme'; const projectInCollection = (project, collection) => collection.items.find((item) => item.projectId === project.id) != null; -const CollectionAddSketchWrapper = styled.div` +export const CollectionAddSketchWrapper = styled.div` width: ${remSize(600)}; max-width: 100%; overflow: auto; `; -const QuickAddWrapper = styled.div` +export const QuickAddWrapper = styled.div` width: ${remSize(600)}; max-width: 100%; padding: ${remSize(24)}; diff --git a/client/modules/IDE/components/AddToCollectionSketchList.jsx b/client/modules/IDE/components/AddToCollectionSketchList.jsx index de26dd7c43..f6cdf3abbf 100644 --- a/client/modules/IDE/components/AddToCollectionSketchList.jsx +++ b/client/modules/IDE/components/AddToCollectionSketchList.jsx @@ -5,7 +5,6 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { withTranslation } from 'react-i18next'; // import find from 'lodash/find'; -import styled from 'styled-components'; import * as ProjectsActions from '../actions/projects'; import * as CollectionsActions from '../actions/collections'; import * as ToastActions from '../actions/toast'; @@ -13,20 +12,10 @@ import * as SortingActions from '../actions/sorting'; import getSortedSketches from '../selectors/projects'; import Loader from '../../App/components/loader'; import QuickAddList from './QuickAddList'; -import { remSize } from '../../../theme'; - -const CollectionAddSketchWrapper = styled.div` - width: ${remSize(600)}; - max-width: 100%; - overflow: auto; -`; - -const QuickAddWrapper = styled.div` - width: ${remSize(600)}; - max-width: 100%; - padding: ${remSize(24)}; - height: 100%; -`; +import { + CollectionAddSketchWrapper, + QuickAddWrapper +} from './AddToCollectionList'; class SketchList extends React.Component { constructor(props) { From e63cb03f3f4e9e1872d8ee6d933c38d11ca772b2 Mon Sep 17 00:00:00 2001 From: Linda Paiste Date: Thu, 10 Aug 2023 20:02:28 -0500 Subject: [PATCH 15/17] Use `selectRootFile` in Nav. --- client/modules/IDE/components/Header/Nav.jsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/client/modules/IDE/components/Header/Nav.jsx b/client/modules/IDE/components/Header/Nav.jsx index 3750033aaa..37709945c4 100644 --- a/client/modules/IDE/components/Header/Nav.jsx +++ b/client/modules/IDE/components/Header/Nav.jsx @@ -14,6 +14,7 @@ import { setLanguage } from '../../actions/preferences'; import NavBar from '../../../../components/Nav/NavBar'; import CaretLeftIcon from '../../../../images/left-arrow.svg'; import LogoIcon from '../../../../images/p5js-logo-small.svg'; +import { selectRootFile } from '../../selectors/files'; import { selectSketchPath } from '../../selectors/project'; import { metaKey, metaKeyName } from '../../../../utils/metaKey'; import { useSketchActions } from '../../hooks'; @@ -102,17 +103,14 @@ const DashboardMenu = () => { ); }; -const ProjectMenu = (props) => { +const ProjectMenu = () => { const isUserOwner = useSelector(getIsUserOwner); const project = useSelector((state) => state.project); const user = useSelector((state) => state.user); const isUnsaved = !project?.id; - // TODO: use selectRootFile selector - const rootFile = useSelector( - (state) => state.files.filter((file) => file.name === 'root')[0] - ); + const rootFile = useSelector(selectRootFile); const cmRef = useContext(CmControllerContext); From 20eadb01c5aa4427e8aae397da6042b1d5b7feb8 Mon Sep 17 00:00:00 2001 From: dewanshDT Date: Fri, 11 Aug 2023 23:33:53 +0530 Subject: [PATCH 16/17] fixed height overlay media query --- client/styles/components/_overlay.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/styles/components/_overlay.scss b/client/styles/components/_overlay.scss index fc59987ce4..83e6124648 100644 --- a/client/styles/components/_overlay.scss +++ b/client/styles/components/_overlay.scss @@ -56,7 +56,11 @@ /* Fixed height overlay */ .overlay--is-fixed-height .overlay__body { - height: 80vh; + height: 100vh; + + @media (min-width: 770px) { + height: 80vh; + } } .overlay--is-fixed-height .overlay__header { From 65f8ad75a7a8b2b3d54da6960b10ca5e7ec37eab Mon Sep 17 00:00:00 2001 From: dewanshDT Date: Fri, 11 Aug 2023 23:45:20 +0530 Subject: [PATCH 17/17] overlay height --- client/styles/components/_overlay.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/styles/components/_overlay.scss b/client/styles/components/_overlay.scss index 83e6124648..089fa4d824 100644 --- a/client/styles/components/_overlay.scss +++ b/client/styles/components/_overlay.scss @@ -57,10 +57,6 @@ /* Fixed height overlay */ .overlay--is-fixed-height .overlay__body { height: 100vh; - - @media (min-width: 770px) { - height: 80vh; - } } .overlay--is-fixed-height .overlay__header {