Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Session vanhenemisen estäminen viestiä kirjoittaessa #5828

Merged
merged 4 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions frontend/src/citizen-frontend/messages/MessageEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ import FileUpload, {
UploadStatus
} from 'lib-components/molecules/FileUpload'
import { InfoBox } from 'lib-components/molecules/MessageBoxes'
import SessionExpiredModal from 'lib-components/molecules/modals/SessionExpiredModal'
import { Bold, P } from 'lib-components/typography'
import { useKeepSessionAlive } from 'lib-components/useKeepSessionAlive'
import { defaultMargins, Gap } from 'lib-components/white-space'
import colors from 'lib-customizations/common'
import { faTimes } from 'lib-icons'
Expand All @@ -49,6 +51,8 @@ import { useUser } from '../auth/state'
import { deleteAttachment } from '../generated/api-clients/attachment'
import { useTranslation } from '../localization'

import { sessionKeepalive } from './utils'

const emptyMessage: CitizenMessageBody = {
title: '',
content: '',
Expand Down Expand Up @@ -127,6 +131,11 @@ export default React.memo(function MessageEditor({
.catch((e) => Failure.fromError<void>(e)),
[]
)
const {
keepSessionAlive,
showSessionExpiredModal,
setShowSessionExpiredModal
} = useKeepSessionAlive(sessionKeepalive)

useEffect(
() =>
Expand Down Expand Up @@ -386,6 +395,7 @@ export default React.memo(function MessageEditor({
}))
}
data-qa="input-content"
onKeyUp={keepSessionAlive}
/>
</TextAreaLabel>

Expand Down Expand Up @@ -433,6 +443,11 @@ export default React.memo(function MessageEditor({
</FormArea>
</Container>
</FocusLock>
{showSessionExpiredModal && (
<SessionExpiredModal
onClose={() => setShowSessionExpiredModal(false)}
/>
)}
</ModalAccessibilityWrapper>
)
})
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/citizen-frontend/messages/ThreadView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import { ConfirmDeleteThread } from './ConfirmDeleteThread'
import { isPrimaryRecipient } from './MessageEditor'
import { replyToThreadMutation } from './queries'
import { MessageContext } from './state'
import { sessionKeepalive } from './utils'

const TitleRow = styled.div`
display: flex;
Expand Down Expand Up @@ -365,6 +366,7 @@ export default React.memo(
replyContent={replyContent}
sendEnabled={sendEnabled}
messageThreadSensitive={sensitive}
sessionKeepAlive={sessionKeepalive}
/>
</ReplyEditorContainer>
) : (
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/citizen-frontend/messages/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2017-2024 City of Espoo
//
// SPDX-License-Identifier: LGPL-2.1-or-later

export const sessionKeepalive = async () => {
const response = await fetch('/api/application/auth/status', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could maybe be moved to lib-commons with some parameterisation if the only difference is the api base path, but not necessary by any means

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tarvitaanko uutta fetch-pohjaista kutsua tälle? Meillähän on jo auth status endpointille funktio

method: 'GET',
credentials: 'include'
})
if (!response.ok) {
// could be e.g. temporarily offline
throw new Error(`HTTP error! status: ${response.status}`)
}

try {
const data = (await response.json()) as { loggedIn?: boolean }
return data.loggedIn === true
} catch (error) {
throw new Error('Failed to parse JSON response')
}
}
1 change: 1 addition & 0 deletions frontend/src/e2e-test/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ declare global {
captureScreenshots: (namePrefix: string) => Promise<void>
saveTraces: (namePrefix: string) => Promise<void>
promises: Promise<void>[]
keepSessionAliveThrottleTime?: number
}
| undefined
}
Expand Down
26 changes: 15 additions & 11 deletions frontend/src/e2e-test/pages/citizen/citizen-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class MockStrongAuthPage {
}
}
export default class CitizenMessagesPage {
#messageReplyContent: TextInput
messageReplyContent: TextInput
#threadListItem: Element
#threadTitle: Element
#redactedThreadTitle: Element
Expand All @@ -38,7 +38,7 @@ export default class CitizenMessagesPage {
newMessageButton: Element
fileUpload: Element
constructor(private readonly page: Page) {
this.#messageReplyContent = new TextInput(
this.messageReplyContent = new TextInput(
page.findByDataQa('message-reply-content')
)
this.#threadListItem = page.findByDataQa('thread-list-item')
Expand Down Expand Up @@ -141,23 +141,27 @@ export default class CitizenMessagesPage {
await this.discardMessageButton.click()
}

async fillReplyContent(content: string) {
await this.#messageReplyContent.fill(content)
}

async assertReplyContentIsEmpty() {
return this.#messageReplyContent.assertTextEquals('')
}

async replyToFirstThread(content: string) {
await this.startReplyToFirstThread()
await this.messageReplyContent.fill(content)
await this.sendReply()
}
async startReplyToFirstThread() {
await this.#threadListItem.click()
await this.#openReplyEditorButton.click()
await this.#messageReplyContent.fill(content)
}
async sendReply() {
await this.#sendReplyButton.click()
// the editor is hidden after sending the reply
await this.#sendReplyButton.waitUntilHidden()
}

async getSessionExpiry() {
const cookies = await this.page.page.context().cookies()
const sessionCookie = cookies.find((c) => c.name === 'evaka.eugw.session')
return sessionCookie?.expires ?? 0
}

async deleteFirstThread() {
await this.#threadListItem.findByDataQa('delete-thread-btn').click()
}
Expand Down
48 changes: 46 additions & 2 deletions frontend/src/e2e-test/specs/7_messaging/messaging.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -686,11 +686,11 @@ describe('Sending and receiving messages', () => {
const citizenMessagesPage = new CitizenMessagesPage(citizenPage)
await citizenMessagesPage.openFirstThreadReplyEditor()
await citizenMessagesPage.discardMessageButton.waitUntilVisible()
await citizenMessagesPage.fillReplyContent(defaultContent)
await citizenMessagesPage.messageReplyContent.fill(defaultContent)
await citizenMessagesPage.discardReplyEditor()
await citizenMessagesPage.discardMessageButton.waitUntilHidden()
await citizenMessagesPage.openFirstThreadReplyEditor()
await citizenMessagesPage.assertReplyContentIsEmpty()
await citizenMessagesPage.messageReplyContent.assertTextEquals('')
})

test('Citizen can reply to a thread and receive a notification on success', async () => {
Expand All @@ -713,6 +713,50 @@ describe('Sending and receiving messages', () => {
)
})

test('Citizen session is kept alive as long as user keeps typing', async () => {
await openSupervisorPage(mockedDateAt10)
await unitSupervisorPage.goto(`${config.employeeUrl}/messages`)
const messagesPage = new MessagesPage(unitSupervisorPage)
const messageEditor = await messagesPage.openMessageEditor()
await messageEditor.sendNewMessage(defaultMessage)
await runPendingAsyncJobs(mockedDateAt10.addMinutes(1))

await openCitizen(mockedDateAt11)

await citizenPage.goto(config.enduserMessagesUrl)
await citizenPage.page.evaluate(() => {
if (window.evaka) window.evaka.keepSessionAliveThrottleTime = 300 // Set to 300ms for tests
})
const citizenMessagesPage = new CitizenMessagesPage(citizenPage)
await citizenMessagesPage.assertThreadContent(defaultMessage)

const initialExpiry = await citizenMessagesPage.getSessionExpiry()

await citizenMessagesPage.startReplyToFirstThread()

const slowTypedText =
'Olen aika hidas kirjoittamaan näitä viestejä, mutta yritän parhaani: Lapseni ovat sitä ja tätä...'

const msDelayPerCharToTypeSlowly = 1000 / slowTypedText.length
const authStatusRequests: string[] = []
citizenPage.page.on('request', (request) => {
if (request.url().includes('/api/application/auth/status')) {
authStatusRequests.push(request.url())
}
})
// typing this takes so long that session keepalive mechanism should renew the session
await citizenMessagesPage.messageReplyContent.locator.pressSequentially(
slowTypedText,
{
delay: msDelayPerCharToTypeSlowly
}
)

const finalExpiry = await citizenMessagesPage.getSessionExpiry()
expect(authStatusRequests.length).toBeGreaterThanOrEqual(3)
expect(finalExpiry).toBeGreaterThan(initialExpiry + 0.3)
})

describe('Messages can be deleted / archived', () => {
test('Unit supervisor sends message and citizen deletes the message', async () => {
await openSupervisorPage(mockedDateAt10)
Expand Down
Loading
Loading