diff --git a/e2e/reset.spec.ts b/e2e/reset.spec.ts new file mode 100644 index 0000000..d5748db --- /dev/null +++ b/e2e/reset.spec.ts @@ -0,0 +1,30 @@ +import { TaFixture } from '../test/fixture/timedAutomatonFixture'; +import { TEST_BASE_URL } from './helper/endToEndTestConstants'; +import { test } from './helper/testOptions'; +import { expect } from '@playwright/test'; + +test.describe('For resetting the TA', () => { + test.beforeEach(async ({ page }) => { + await page.goto(TEST_BASE_URL); + }); + + test('the initial page should contain an enabled button for resetting', async ({ page }) => { + await expect(page.getByTestId('button-open-reset')).toBeVisible(); + await expect(page.getByTestId('button-open-reset')).toBeEnabled(); + }); + + test('the reset button should reset the TA to its original state', async ({ page, taUiHelper }) => { + // given + const initialTa = await taUiHelper.readTaFromUi(); + const otherTa = TaFixture.withTwoLocationsAndTwoSwitches(); + await taUiHelper.setTimedAutomatonTo(otherTa); + + // when + await page.getByTestId('button-open-reset').click(); + await page.getByTestId('button-confirm-ta-reset').click(); + + // then + const taAfterReset = await taUiHelper.readTaFromUi(); + expect(taAfterReset, 'TA after reset should be the same as the initial TA').toEqual(initialTa); + }); +}); diff --git a/eslint.config.js b/eslint.config.js index 00371a7..5883140 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -53,6 +53,6 @@ export default tseslint.config( }, { // needs to be in its own object to act as global ignore - ignores: ['dist', '*.cjs'], + ignores: ['dist', 'playwright-report', '*.cjs'], } ); diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json index 563fff7..e047ed5 100644 --- a/public/locales/de/translation.json +++ b/public/locales/de/translation.json @@ -1,6 +1,7 @@ { "app.title": "Analyse von Timed Automata", "manipulation.button.reachability": "Erreichbarkeit", + "manipulation.button.reset": "TA zurücksetzen", "manipulation.table.showContent": "{{content}} ausklappen", "manipulation.table.hideContent": "{{content}} einklappen", "manipulation.table.addElement": "{{content}} hinzufügen", @@ -21,6 +22,10 @@ "analysisDialog.analysis.resultSomeUnreachable": "Die folgenden Orte sind unerreichbar:", "analysisDialog.button.close": "Schließen", "analysisDialog.button.analyze": "Starten", + "resetDialog.title": "Zurücksetzen bestätigen", + "resetDialog.contentText": "Bist du sicher, dass du den TA zurücksetzen möchtest? Alle Änderungen gehen dabei verloren.", + "resetDialog.button.cancel": "Abbrechen", + "resetDialog.button.reset": "Zurücksetzen", "locDialog.errorNameEmpty": "Name darf nicht leer sein", "locDialog.errorNameExists": "Name wird bereits verwendet", "locDialog.editLoc": "Ort bearbeiten", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 6de7ada..be1f978 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -1,6 +1,7 @@ { "app.title": "Timed-Automata Analysis", "manipulation.button.reachability": "Reachability", + "manipulation.button.reset": "Reset TA", "manipulation.table.showContent": "Show {{content}}", "manipulation.table.hideContent": "Hide {{content}}", "manipulation.table.addElement": "Add {{content}}", @@ -21,6 +22,10 @@ "analysisDialog.analysis.resultSomeUnreachable": "The following locations are unreachable:", "analysisDialog.button.close": "Close", "analysisDialog.button.analyze": "Analyze", + "resetDialog.title": "Confirm Reset", + "resetDialog.contentText": "Are you sure that you want to reset the TA? All changes will be lost.", + "resetDialog.button.cancel": "Cancel", + "resetDialog.button.reset": "Reset", "locDialog.errorNameEmpty": "Name cannot be empty", "locDialog.errorNameExists": "Name already exists", "locDialog.editLoc": "Edit Location", diff --git a/src/utils/initAutomaton.ts b/src/utils/initAutomaton.ts index f085683..e89e30d 100644 --- a/src/utils/initAutomaton.ts +++ b/src/utils/initAutomaton.ts @@ -53,8 +53,12 @@ const SWITCH_0: Switch = { target: LOC_1, }; -export const INIT_AUTOMATON: TimedAutomaton = { +const INIT_AUTOMATON: TimedAutomaton = { locations: [LOC_0, LOC_1], clocks: [CLOCK_0, CLOCK_1], switches: [SWITCH_0], }; + +export function getInitAutomaton(): TimedAutomaton { + return structuredClone(INIT_AUTOMATON); +} diff --git a/src/view/AutomatonManipulation.tsx b/src/view/AutomatonManipulation.tsx index 519976e..40b1ac6 100644 --- a/src/view/AutomatonManipulation.tsx +++ b/src/view/AutomatonManipulation.tsx @@ -16,6 +16,7 @@ import ClockDeleteConfirmDialog from './ClockDeleteConfirmDialog'; import ManipulateClockDialog from './ManipulateClockDialog'; import { AnalysisDialog } from './AnalysisDialog'; import { useButtonUtils } from '../utils/buttonUtils'; +import { ResetDialog } from './ResetDialog'; interface ManipulationProps { viewModel: AnalysisViewModel; @@ -35,6 +36,7 @@ export const AutomatonManipulation: React.FC = (props) => { addClock, editClock, removeClock, + setStateReset, } = viewModel; const { locations, switches, clocks } = ta; const { t } = useTranslation(); @@ -275,25 +277,45 @@ export const AutomatonManipulation: React.FC = (props) => { const handleAnalysisOpen = () => setAnalysisOpen(true); const handleAnalysisClose = () => setAnalysisOpen(false); + // ===== handle reset ======================================================== + + const [resetWarnOpen, setResetWarnOpen] = useState(false); + const handleResetOpen = () => setResetWarnOpen(true); + const handleResetClose = () => setResetWarnOpen(false); + const handleReset = () => setStateReset(viewModel); + // =========================================================================== return ( <> -
- -
+ +
+ {allTables} + = (props return ( - {t('deleteClockConfirmDialog.title', { clockName: clock.name })} + + {t('deleteClockConfirmDialog.title', { clockName: clock.name })} + executeOnKeyboardClick(e.key, onClose)} + sx={{ position: 'absolute', right: 8, top: 8, color: (theme) => theme.palette.grey[500] }} + > + + + {t('deleteClockConfirmDialog.contentText', { clockName: clock.name })}

diff --git a/src/view/ResetDialog.tsx b/src/view/ResetDialog.tsx new file mode 100644 index 0000000..96feb4c --- /dev/null +++ b/src/view/ResetDialog.tsx @@ -0,0 +1,66 @@ +import { useTranslation } from 'react-i18next'; +import { useButtonUtils } from '../utils/buttonUtils'; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + IconButton, +} from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; + +interface ResetDialogProps { + open: boolean; + handleClose: () => void; + handleReset: () => void; +} + +export const ResetDialog: React.FC = (props) => { + const { open, handleClose, handleReset } = props; + const { t } = useTranslation(); + const { executeOnKeyboardClick } = useButtonUtils(); + + const confirmReset = () => { + handleClose(); + handleReset(); + }; + + return ( +

+ + {t('resetDialog.title')} + executeOnKeyboardClick(e.key, handleClose)} + sx={{ position: 'absolute', right: 8, top: 8, color: (theme) => theme.palette.grey[500] }} + > + + + + + {t('resetDialog.contentText')} + + + + + + + ); +}; diff --git a/src/viewmodel/AnalysisViewModel.ts b/src/viewmodel/AnalysisViewModel.ts index a2eeee8..e31b6f8 100644 --- a/src/viewmodel/AnalysisViewModel.ts +++ b/src/viewmodel/AnalysisViewModel.ts @@ -8,7 +8,7 @@ import { useMathUtils } from '../utils/mathUtils'; import { useSwitchUtils } from '../utils/switchUtils'; import { useClockConstraintUtils } from '../utils/clockConstraintUtils'; import { useClockUtils } from '../utils/clockUtils'; -import { INIT_AUTOMATON } from '../utils/initAutomaton'; +import { getInitAutomaton } from '../utils/initAutomaton'; export interface AnalysisViewModel { state: AnalysisState; @@ -57,6 +57,7 @@ export interface AnalysisViewModel { removeClock: (viewModel: AnalysisViewModel, clock: Clock) => void; setStateAnalyzing: (viewModel: AnalysisViewModel) => void; setStateReady: (viewModel: AnalysisViewModel) => void; + setStateReset: (viewModel: AnalysisViewModel) => void; } export enum AnalysisState { @@ -287,24 +288,29 @@ export function useAnalysisViewModel(): AnalysisViewModel { setViewModel({ ...viewModel, state: AnalysisState.READY }); }, []); + const setStateReset = useCallback((viewModel: AnalysisViewModel) => { + setViewModel({ ...viewModel, state: AnalysisState.RESET }); + }, []); + // ===== manipulate state ==================================================== const [viewModel, setViewModel] = useState({ state: AnalysisState.INIT, - ta: INIT_AUTOMATON, - addLocation: addLocation, - editLocation: editLocation, - removeLocation: removeLocation, - setInitialLocation: setInitialLocation, - updateLocationCoordinates: updateLocationCoordinates, - addSwitch: addSwitch, - editSwitch: editSwitch, - removeSwitch: removeSwitch, - addClock: addClock, - editClock: editClock, - removeClock: removeClock, - setStateAnalyzing: setStateAnalyzing, - setStateReady: setStateReady, + ta: getInitAutomaton(), + addLocation, + editLocation, + removeLocation, + setInitialLocation, + updateLocationCoordinates, + addSwitch, + editSwitch, + removeSwitch, + addClock, + editClock, + removeClock, + setStateAnalyzing, + setStateReady, + setStateReset, }); // =================================================================================================================== @@ -318,14 +324,13 @@ export function useAnalysisViewModel(): AnalysisViewModel { useEffect(() => { if (viewModel.state === AnalysisState.ANALYZING) { - setViewModel({ ...viewModel, state: AnalysisState.READY }); + // nothing to do here at the moment } }, [viewModel]); useEffect(() => { if (viewModel.state === AnalysisState.RESET) { - // TODO: add a reset button to use this reset - setViewModel({ ...viewModel, ta: INIT_AUTOMATON, state: AnalysisState.READY }); + setViewModel({ ...viewModel, ta: getInitAutomaton(), state: AnalysisState.READY }); } }, [viewModel]);