🐛 ✅ Fix persistence
* persistSimulation: make it url path specific and make it available to all Simulations ** 1 e2e test with salarié & auto-entrepreur ** typescript test * persistEverything becomes InFranceApp-specific * mon-entreprise Provider initialStore is now partialpull/1380/head
parent
5eec284b13
commit
512bc74820
|
@ -140,18 +140,21 @@ describe('Simulateur salarié', () => {
|
|||
}
|
||||
before(() => cy.visit('/simulateurs/salarié'))
|
||||
|
||||
it('should save the current simulation', function () {
|
||||
cy.get(inputSelector).first().type('{selectall}2137')
|
||||
it('should persist the current simulation (persistSimulation)', function () {
|
||||
cy.get(inputSelector).first().type('{selectall}42')
|
||||
cy.contains('Passer').click()
|
||||
cy.contains('Passer').click()
|
||||
cy.contains('Passer').click()
|
||||
cy.wait(1600)
|
||||
cy.visit('/simulateurs/auto-entrepreneur')
|
||||
cy.get(inputSelector).first().type('{selectall}007')
|
||||
cy.contains('Passer').click()
|
||||
cy.contains('Passer').click()
|
||||
cy.contains('Passer').click()
|
||||
cy.wait(1600)
|
||||
cy.visit('/simulateurs/salarié')
|
||||
cy.contains('Retrouver ma simulation').click()
|
||||
cy.get(inputSelector)
|
||||
.first()
|
||||
.invoke('val')
|
||||
.should('match', /2[\s]137/)
|
||||
cy.get(inputSelector).first().invoke('val').should('match', /42/)
|
||||
})
|
||||
|
||||
it('should not crash when selecting localisation', function () {
|
||||
|
|
|
@ -22,13 +22,10 @@ import {
|
|||
} from 'Selectors/simulationSelectors'
|
||||
import Provider, { ProviderProps } from './Provider'
|
||||
import {
|
||||
persistEverything,
|
||||
retrievePersistedState,
|
||||
} from './storage/persistEverything'
|
||||
import {
|
||||
persistSimulation,
|
||||
retrievePersistedSimulation,
|
||||
} from './storage/persistSimulation'
|
||||
setupInFranceAppPersistence,
|
||||
retrievePersistedInFranceApp,
|
||||
} from './storage/persistInFranceApp'
|
||||
import { setupSimulationPersistence } from './storage/persistSimulation'
|
||||
import Tracker, { devTracker } from './Tracker'
|
||||
import './App.css'
|
||||
import Footer from 'Components/layout/Footer/Footer'
|
||||
|
@ -99,12 +96,11 @@ export default function Root({ basename, rules }: RootProps) {
|
|||
sitePaths={paths}
|
||||
reduxMiddlewares={middlewares}
|
||||
onStoreCreated={(store) => {
|
||||
persistEverything({ except: ['simulation'] })(store)
|
||||
persistSimulation(store)
|
||||
setupInFranceAppPersistence(store)
|
||||
setupSimulationPersistence(store)
|
||||
}}
|
||||
initialStore={{
|
||||
...retrievePersistedState(),
|
||||
previousSimulation: retrievePersistedSimulation(),
|
||||
inFranceApp: retrievePersistedInFranceApp(),
|
||||
}}
|
||||
>
|
||||
<EngineProvider value={engine}>
|
||||
|
|
|
@ -7,6 +7,7 @@ import React, { createContext, useEffect, useMemo } from 'react'
|
|||
import { I18nextProvider } from 'react-i18next'
|
||||
import { Provider as ReduxProvider } from 'react-redux'
|
||||
import { Router } from 'react-router-dom'
|
||||
import { PreloadedState } from 'redux'
|
||||
import reducers, { RootState } from 'Reducers/rootReducer'
|
||||
import { applyMiddleware, compose, createStore, Middleware, Store } from 'redux'
|
||||
import Tracker from './Tracker'
|
||||
|
@ -48,7 +49,7 @@ export type ProviderProps = {
|
|||
children: React.ReactNode
|
||||
tracker?: Tracker
|
||||
sitePaths?: SitePaths
|
||||
initialStore?: RootState
|
||||
initialStore?: PreloadedState<RootState>
|
||||
onStoreCreated?: (store: Store) => void
|
||||
reduxMiddlewares?: Array<Middleware>
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { ThemeColorsProvider } from 'Components/utils/colors'
|
|||
import { IsEmbeddedContext } from 'Components/utils/embeddedContext'
|
||||
import Meta from 'Components/utils/Meta'
|
||||
import useSimulationConfig from 'Components/utils/useSimulationConfig'
|
||||
import PreviousSimulationBanner from 'Components/PreviousSimulationBanner'
|
||||
import { default as React, useContext, useEffect } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { SimulatorData } from './metadata'
|
||||
|
@ -38,6 +39,7 @@ export default function SimulateurPage({
|
|||
|
||||
<ThemeColorsProvider color={inIframe ? undefined : meta?.color}>
|
||||
<Component />
|
||||
{config && <PreviousSimulationBanner />}
|
||||
{seoExplanations && !inIframe && seoExplanations}
|
||||
</ThemeColorsProvider>
|
||||
</>
|
||||
|
|
|
@ -46,7 +46,6 @@ export default function SalariéSimulation() {
|
|||
}
|
||||
/>
|
||||
<br />
|
||||
<PreviousSimulationBanner />
|
||||
|
||||
{/** L'équipe Code Du Travail Numérique ne souhaite pas référencer
|
||||
* le simulateur dirigeant de SASU sur son site. */}
|
||||
|
|
|
@ -164,10 +164,13 @@ function existingCompany(
|
|||
return state
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
const inFranceAppReducer = combineReducers({
|
||||
companyLegalStatus,
|
||||
companyStatusChoice,
|
||||
companyCreationChecklist,
|
||||
existingCompany,
|
||||
hiringChecklist,
|
||||
})
|
||||
export default inFranceAppReducer
|
||||
|
||||
export type InFranceAppState = ReturnType<typeof inFranceAppReducer>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Simulation } from 'Reducers/rootReducer'
|
||||
import { Action } from 'Actions/actions'
|
||||
import { RootState } from './rootReducer'
|
||||
import { retrievePersistedSimulation } from '../storage/persistSimulation'
|
||||
|
||||
export const createStateFromPreviousSimulation = (
|
||||
state: RootState
|
||||
|
@ -19,6 +20,11 @@ export const createStateFromPreviousSimulation = (
|
|||
|
||||
export default (state: RootState, action: Action): RootState => {
|
||||
switch (action.type) {
|
||||
case 'SET_SIMULATION':
|
||||
return {
|
||||
...state,
|
||||
previousSimulation: retrievePersistedSimulation(action.url),
|
||||
}
|
||||
case 'LOAD_PREVIOUS_SIMULATION':
|
||||
return {
|
||||
...state,
|
||||
|
|
|
@ -1,31 +1,26 @@
|
|||
import { Action } from 'Actions/actions'
|
||||
import { omit } from 'ramda'
|
||||
import { RootState } from 'Reducers/rootReducer'
|
||||
import { Store } from 'redux'
|
||||
import { debounce } from '../utils'
|
||||
import safeLocalStorage from './safeLocalStorage'
|
||||
import { InFranceAppState } from '../reducers/inFranceAppReducer'
|
||||
|
||||
const VERSION = 5
|
||||
const VERSION = 6
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'app::global-state:v' + VERSION
|
||||
const LOCAL_STORAGE_KEY = 'mon-entreprise::persisted-infranceapp::v' + VERSION
|
||||
|
||||
type OptionsType = {
|
||||
except?: Array<string>
|
||||
}
|
||||
export const persistEverything = (options: OptionsType = {}) => (
|
||||
store: Store<RootState, Action>
|
||||
): void => {
|
||||
export function setupInFranceAppPersistence(store: Store<RootState, Action>) {
|
||||
const listener = () => {
|
||||
const state = store.getState()
|
||||
safeLocalStorage.setItem(
|
||||
LOCAL_STORAGE_KEY,
|
||||
JSON.stringify(omit(options.except || [], state))
|
||||
JSON.stringify(state.inFranceApp)
|
||||
)
|
||||
}
|
||||
store.subscribe(debounce(1000, listener))
|
||||
}
|
||||
|
||||
export function retrievePersistedState(): RootState {
|
||||
export function retrievePersistedInFranceApp(): InFranceAppState {
|
||||
const serializedState = safeLocalStorage.getItem(LOCAL_STORAGE_KEY)
|
||||
return serializedState ? JSON.parse(serializedState) : null
|
||||
return serializedState ? JSON.parse(serializedState) : undefined
|
||||
}
|
|
@ -5,27 +5,34 @@ import { PreviousSimulation } from 'Selectors/previousSimulationSelectors'
|
|||
import { debounce } from '../utils'
|
||||
import safeLocalStorage from './safeLocalStorage'
|
||||
import { deserialize, serialize } from './serializeSimulation'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
const VERSION = 4
|
||||
const VERSION = 5
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'embauche.gouv.fr::persisted-simulation::v' + VERSION
|
||||
const localStorageKey = (pathname: string) =>
|
||||
`mon-entreprise::persisted-simulation::v${VERSION}::${pathname}`
|
||||
|
||||
export function persistSimulation(store: Store<RootState, Action>) {
|
||||
export function setupSimulationPersistence(
|
||||
store: Store<RootState, Action>,
|
||||
debounceDelay: number = 1000
|
||||
) {
|
||||
const listener = () => {
|
||||
const state = store.getState()
|
||||
if (!state.simulation?.foldedSteps.length) {
|
||||
return
|
||||
}
|
||||
safeLocalStorage.setItem(LOCAL_STORAGE_KEY, serialize(state))
|
||||
if (!state.simulation?.url) return
|
||||
if (!state.simulation?.foldedSteps.length) return
|
||||
safeLocalStorage.setItem(
|
||||
localStorageKey(state.simulation.url),
|
||||
serialize(state)
|
||||
)
|
||||
}
|
||||
store.subscribe(debounce(1000, listener))
|
||||
store.subscribe(debounce(debounceDelay, listener))
|
||||
}
|
||||
|
||||
export function retrievePersistedSimulation(): SavedSimulation {
|
||||
const serializedState = safeLocalStorage.getItem(LOCAL_STORAGE_KEY)
|
||||
export function retrievePersistedSimulation(
|
||||
simulationUrl: string
|
||||
): PreviousSimulation | null {
|
||||
const serializedState = safeLocalStorage.getItem(
|
||||
localStorageKey(simulationUrl)
|
||||
)
|
||||
return serializedState ? deserialize(serializedState) : null
|
||||
}
|
||||
|
||||
export function deletePersistedSimulation(): void {
|
||||
safeLocalStorage.removeItem(LOCAL_STORAGE_KEY)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
import { expect } from 'chai'
|
||||
import * as sinon from 'sinon'
|
||||
import { createStore } from 'redux'
|
||||
import { createMemoryHistory } from 'history'
|
||||
|
||||
import { DottedName } from 'modele-social'
|
||||
import reducers, {
|
||||
Simulation,
|
||||
SimulationConfig,
|
||||
} from '../source/reducers/rootReducer'
|
||||
import { PreviousSimulation } from '../source/selectors/previousSimulationSelectors'
|
||||
import safeLocalStorage from '../source/storage/safeLocalStorage'
|
||||
import { setupSimulationPersistence } from '../source/storage/persistSimulation'
|
||||
import {
|
||||
loadPreviousSimulation,
|
||||
updateSituation,
|
||||
setSimulationConfig,
|
||||
} from '../source/actions/actions'
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
const simulationConfig: SimulationConfig = {
|
||||
objectifs: [],
|
||||
'objectifs cachés': [],
|
||||
situation: {},
|
||||
'unité par défaut': '€/mois',
|
||||
}
|
||||
const initialSimulation: Simulation = {
|
||||
config: simulationConfig,
|
||||
url: '/someurl',
|
||||
hiddenNotifications: [],
|
||||
situation: {},
|
||||
initialSituation: {},
|
||||
targetUnit: '€/mois',
|
||||
foldedSteps: ['somestep' as DottedName],
|
||||
unfoldedStep: null,
|
||||
}
|
||||
|
||||
describe('[persistence] When simulation persistence is setup', () => {
|
||||
const sandbox = sinon.createSandbox()
|
||||
let spiedSafeLocalStorage
|
||||
let store
|
||||
|
||||
beforeEach(() => {
|
||||
spiedSafeLocalStorage = sandbox.spy(safeLocalStorage as any)
|
||||
store = createStore(reducers, {
|
||||
simulation: initialSimulation,
|
||||
activeTargetInput: 'sometargetinput',
|
||||
})
|
||||
|
||||
setupSimulationPersistence(store, 0)
|
||||
})
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
describe('when the state is changed with some data that is persistable', () => {
|
||||
beforeEach(async () => {
|
||||
store.dispatch(updateSituation('dotted name' as DottedName, '42'))
|
||||
await delay(0)
|
||||
})
|
||||
it('saves state in localStorage with all fields', () => {
|
||||
expect(spiedSafeLocalStorage.setItem.calledOnce).to.be.true
|
||||
expect(spiedSafeLocalStorage.setItem.getCall(0).args[1]).to.eq(
|
||||
'{"situation":{"dotted name":"42"},"activeTargetInput":"sometargetinput","foldedSteps":["somestep"]}'
|
||||
)
|
||||
})
|
||||
it('saves state in localStorage with a key dependent on the simulation url', () => {
|
||||
expect(spiedSafeLocalStorage.setItem.calledOnce).to.be.true
|
||||
expect(spiedSafeLocalStorage.setItem.getCall(0).args[0]).to.contain(
|
||||
'someurl'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('[persistence] When simulation config is set', () => {
|
||||
const serializedPreviousSimulation =
|
||||
'{"situation":{"dotted name . other":"42"},"activeTargetInput":"someothertargetinput","foldedSteps":["someotherstep"]}'
|
||||
|
||||
const sandbox = sinon.createSandbox()
|
||||
let store
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox
|
||||
.stub(safeLocalStorage, 'getItem')
|
||||
.callsFake(() => serializedPreviousSimulation)
|
||||
store = createStore(reducers)
|
||||
const history = createMemoryHistory()
|
||||
history.replace('/someotherurl')
|
||||
|
||||
store.dispatch(
|
||||
setSimulationConfig(simulationConfig, history.location.pathname)
|
||||
)
|
||||
})
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
describe('when previous simulation is loaded in state', () => {
|
||||
beforeEach(() => {
|
||||
store.dispatch(loadPreviousSimulation())
|
||||
})
|
||||
it('loads url in state', () => {
|
||||
expect(store.getState().simulation.url).to.eq('/someotherurl')
|
||||
})
|
||||
it('loads situation in state', () => {
|
||||
expect(store.getState().simulation.situation).to.deep.eq({
|
||||
'dotted name . other': '42',
|
||||
})
|
||||
})
|
||||
it('loads activeTargetInput in state', () => {
|
||||
expect(store.getState().activeTargetInput).to.eq('someothertargetinput')
|
||||
})
|
||||
it('loads foldedSteps in state', () => {
|
||||
expect(store.getState().simulation.foldedSteps).to.deep.eq([
|
||||
'someotherstep',
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue