diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d965fbaa6b..5b7d34fe81 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ okio = "3.3.0" serialization = "1.6.0" kotlinx-datetime = "0.4.1" kotlinx-coroutines = "1.7.3" -kotlin-wrappers = "1.0.0-pre.634" +kotlin-wrappers = "1.0.0-pre.635" spring-boot = "2.7.17" spring-cloud = "3.1.8" spring-cloud-kubernetes = "2.1.8" diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/App.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/App.kt index d3e4b15466..ff0197c7f1 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/App.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/App.kt @@ -4,21 +4,26 @@ package com.saveourtool.save.frontend -import com.saveourtool.save.frontend.components.* import com.saveourtool.save.frontend.components.basic.cookieBanner import com.saveourtool.save.frontend.components.basic.scrollToTopButton +import com.saveourtool.save.frontend.components.errorView +import com.saveourtool.save.frontend.components.footer +import com.saveourtool.save.frontend.components.requestModalHandler import com.saveourtool.save.frontend.components.topbar.topBarComponent import com.saveourtool.save.frontend.externals.i18next.initI18n import com.saveourtool.save.frontend.externals.modal.ReactModal -import com.saveourtool.save.frontend.routing.basicRouting +import com.saveourtool.save.frontend.routing.createBasicRoutes import com.saveourtool.save.frontend.utils.* import com.saveourtool.save.info.UserInfo import com.saveourtool.save.validation.FrontendRoutes +import js.core.jso import react.* import react.dom.client.createRoot import react.dom.html.ReactHTML.div -import react.router.dom.BrowserRouter +import react.router.Outlet +import react.router.dom.RouterProvider +import react.router.dom.createBrowserRouter import web.cssom.ClassName import web.dom.document import web.html.HTMLElement @@ -48,32 +53,43 @@ val App: FC = FC { } } } - BrowserRouter { - basename = "/" + + val root = FC { requestModalHandler { this.userInfo = userInfo div { className = ClassName("d-flex flex-column") id = "content-wrapper" - ErrorBoundary::class.react { - topBarComponent { this.userInfo = userInfo } - div { - className = ClassName("container-fluid") - id = "common-save-container" - basicRouting { - this.userInfo = userInfo - this.userInfoSetter = setUserInfo - } - } - if (kotlinx.browser.window.location.pathname != "/${FrontendRoutes.COOKIE}") { - cookieBanner { } - } - footer { } + topBarComponent { this.userInfo = userInfo } + div { + className = ClassName("container-fluid") + id = "common-save-container" + Outlet() + } + if (window.location.pathname != "/${FrontendRoutes.COOKIE}") { + cookieBanner { } } + footer { } } } scrollToTopButton() } + + RouterProvider { + router = createBrowserRouter( + routes = arrayOf( + jso { + path = "/" + element = root.create() + errorElement = errorView.create() + children = createBasicRoutes(userInfo, setUserInfo) + } + ), + opts = jso { + basename = "/" + } + ) + } } fun main() { diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/ErrorBoundary.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/ErrorBoundary.kt deleted file mode 100644 index 3cc2a20488..0000000000 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/ErrorBoundary.kt +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Support for [error boundaries](https://reactjs.org/docs/error-boundaries.html) - */ - -package com.saveourtool.save.frontend.components - -import com.saveourtool.save.frontend.components.topbar.topBarComponent -import com.saveourtool.save.frontend.components.views.FallbackView - -import js.core.jso -import react.* -import react.dom.html.ReactHTML.div -import web.cssom.ClassName - -/** - * State of error boundary component - */ -external interface ErrorBoundaryState : State { - /** - * The error message - */ - var errorMessage: String? - - /** - * True is there is an error in the wrapped component tree - */ - var hasError: Boolean? -} - -/** - * Component to act as React Error Boundary - */ -class ErrorBoundary : Component() { - init { - state = jso { - errorMessage = null - hasError = false - } - } - - override fun render(): ReactNode? = if (state.hasError == true) { - FC { - div { - className = ClassName("container-fluid") - topBarComponent() - FallbackView::class.react { - bigText = "Error" - smallText = "Something went wrong: ${state.errorMessage ?: "Unknown error"}" - } - @Suppress("EMPTY_BLOCK_STRUCTURE_ERROR") - footer { } - } - }.create() - } else { - props.children - } - - companion object : RStatics(ErrorBoundary::class) { - init { - /* - * From [React docs](https://reactjs.org/docs/error-boundaries.html): - * 'A class component becomes an error boundary if it defines either (or both) of the lifecycle methods static getDerivedStateFromError() or componentDidCatch()' - */ - getDerivedStateFromError = { ex -> - jso { - errorMessage = ex.message - hasError = true - } - } - } - } -} diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/ErrorView.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/ErrorView.kt new file mode 100644 index 0000000000..7faf7914ca --- /dev/null +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/ErrorView.kt @@ -0,0 +1,28 @@ +/** + * A page for errors + */ + +package com.saveourtool.save.frontend.components + +import com.saveourtool.save.frontend.components.topbar.topBarComponent +import com.saveourtool.save.frontend.components.views.FallbackView +import js.errors.JsError +import react.FC +import react.dom.html.ReactHTML.div +import react.react +import react.router.useRouteError +import web.cssom.ClassName + +val errorView = FC { + val errorMessage = useRouteError().unsafeCast().message + div { + className = ClassName("container-fluid") + topBarComponent() + FallbackView::class.react { + bigText = "Error" + smallText = "Something went wrong: $errorMessage" + } + @Suppress("EMPTY_BLOCK_STRUCTURE_ERROR") + footer { } + } +} diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/topbar/TopBarUserField.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/topbar/TopBarUserField.kt index aed6c5a9c2..aff78abb45 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/topbar/TopBarUserField.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/topbar/TopBarUserField.kt @@ -87,7 +87,7 @@ val topBarUserField: FC = FC { props -> img { className = ClassName("ml-2 align-self-center avatar avatar-user width-full border color-bg-default rounded-circle fas mr-2") - src = props.userInfo?.avatar?.avatarRenderer() ?: AVATAR_PROFILE_PLACEHOLDER + src = userInfo.avatar?.avatarRenderer() ?: AVATAR_PROFILE_PLACEHOLDER style = logoSize } } ?: fontAwesomeIcon(icon = faUser) { diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/usersettings/SettingsViewLeftColumn.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/usersettings/SettingsViewLeftColumn.kt index 153bc1a283..aceb4407e8 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/usersettings/SettingsViewLeftColumn.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/usersettings/SettingsViewLeftColumn.kt @@ -28,7 +28,6 @@ import web.cssom.rem internal const val AVATAR_TITLE = "Upload avatar" val leftSettingsColumn: FC = FC { props -> - val (avatarImgLink, setAvatarImgLink) = useState(null) val (t) = useTranslation("profile") div { @@ -50,8 +49,7 @@ val leftSettingsColumn: FC = FC { props -> className = ClassName("row justify-content-center") img { className = ClassName("avatar avatar-user width-full border color-bg-default rounded-circle") - src = avatarImgLink - ?: props.userInfo?.avatar?.avatarRenderer() + src = props.userInfo?.avatar?.avatarRenderer() ?: AVATAR_PROFILE_PLACEHOLDER style = jso { height = 12.rem diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/routing/BasicRouting.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/routing/BasicRouting.kt index d25e115318..cf54386316 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/routing/BasicRouting.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/routing/BasicRouting.kt @@ -28,27 +28,55 @@ import com.saveourtool.save.frontend.components.views.welcome.saveWelcomeView import com.saveourtool.save.frontend.components.views.welcome.vulnWelcomeView import com.saveourtool.save.frontend.utils.* import com.saveourtool.save.frontend.utils.isSuperAdmin +import com.saveourtool.save.info.UserInfo import com.saveourtool.save.validation.FrontendRoutes.* +import js.core.jso import org.w3c.dom.url.URLSearchParams import react.* import react.router.* +private val fallbackElementWithoutRouterLink = FallbackView::class.react.create { + bigText = "404" + smallText = "Page not found" + withRouterLink = false +} + +private val fallbackElementWithRouterLink = FallbackView::class.react.create { + bigText = "404" + smallText = "Page not found" + withRouterLink = true +} + /** * Just put a map: View -> Route URL to this list + * + * @param userInfo currently logged-in user or null + * @param userInfoSetter setter of user info (it can be updated in settings on several views) + * @return array of [RouteObject] */ -val basicRouting: FC = FC { props -> - useUserStatusRedirects(props.userInfo?.status) +@Suppress( + "TOO_LONG_FUNCTION", + "LongMethod", +) +fun createBasicRoutes( + userInfo: UserInfo?, + userInfoSetter: StateSetter, +): Array { + val indexRoute: RouteObject = jso { + index = true + element = indexView.create { this.userInfo = userInfo } + } val userProfileView = withRouter { _, params -> userProfileView { userName = params["name"]!! - currentUserInfo = props.userInfo + currentUserInfo = userInfo } } val contestView = withRouter { location, params -> ContestView::class.react { - currentUserInfo = props.userInfo + currentUserInfo = userInfo currentContestName = params["contestName"] this.location = location } @@ -56,7 +84,7 @@ val basicRouting: FC = FC { props -> val contestExecutionView = withRouter { _, params -> ContestExecutionView::class.react { - currentUserInfo = props.userInfo + currentUserInfo = userInfo contestName = params["contestName"]!! organizationName = params["organizationName"]!! projectName = params["projectName"]!! @@ -67,7 +95,7 @@ val basicRouting: FC = FC { props -> ProjectView::class.react { name = params["name"]!! owner = params["owner"]!! - currentUserInfo = props.userInfo + currentUserInfo = userInfo this.location = location } } @@ -99,7 +127,7 @@ val basicRouting: FC = FC { props -> val organizationView = withRouter { location, params -> OrganizationView::class.react { organizationName = params["owner"]!! - currentUserInfo = props.userInfo + currentUserInfo = userInfo this.location = location } } @@ -121,7 +149,7 @@ val basicRouting: FC = FC { props -> val contestTemplateView = withRouter { _, params -> contestTemplateView { id = requireNotNull(params["id"]).toLong() - currentUserInfo = props.userInfo + currentUserInfo = userInfo } } @@ -136,7 +164,7 @@ val basicRouting: FC = FC { props -> val vulnerabilityCollectionView = withRouter { location, _ -> vulnerabilityCollectionView { - currentUserInfo = props.userInfo + currentUserInfo = userInfo filter = URLSearchParams(location.search).toVulnerabilitiesFilter() } } @@ -144,7 +172,7 @@ val basicRouting: FC = FC { props -> val vulnerabilityView = withRouter { _, params -> vulnerabilityView { identifier = requireNotNull(params["identifier"]) - currentUserInfo = props.userInfo + currentUserInfo = userInfo } } @@ -163,125 +191,109 @@ val basicRouting: FC = FC { props -> } } - Routes { - listOf( - indexView.create { userInfo = props.userInfo } to "/", - saveWelcomeView.create { userInfo = props.userInfo } to SAVE, - vulnWelcomeView.create { userInfo = props.userInfo } to VULN, - sandboxView.create() to SANDBOX, - AboutUsView::class.react.create() to ABOUT_US, - createOrganizationView.create() to CREATE_ORGANIZATION, - registrationView.create { - userInfo = props.userInfo - userInfoSetter = props.userInfoSetter - } to REGISTRATION, - CollectionView::class.react.create { currentUserInfo = props.userInfo } to PROJECTS, - contestListView.create { currentUserInfo = props.userInfo } to CONTESTS, - - FallbackView::class.react.create { - bigText = "404" - smallText = "Page not found" - withRouterLink = true - } to ERROR_404, - banView.create { userInfo = props.userInfo } to BAN, - contestGlobalRatingView.create() to CONTESTS_GLOBAL_RATING, - contestView.create() to "$CONTESTS/:contestName", - createContestTemplateView.create() to CREATE_CONTESTS_TEMPLATE, - contestTemplateView.create() to "$CONTESTS_TEMPLATE/:id", - contestExecutionView.create() to "$CONTESTS/:contestName/:organizationName/:projectName", - awesomeBenchmarksView.create() to AWESOME_BENCHMARKS, - createProjectView.create() to "$CREATE_PROJECT/:organization?", - organizationView.create() to ":owner", - historyView.create() to ":owner/:name/history", - projectView.create() to ":owner/:name", - createProjectProblemView.create() to "project/:owner/:name/security/problems/new", - projectProblemView.create() to "project/:owner/:name/security/problems/:id", - executionView.create() to ":organization/:project/history/execution/:executionId", - demoView.create() to "$DEMO/:organizationName/:projectName", - cpgView.create() to "$DEMO/cpg", - testExecutionDetailsView.create() to "/:organization/:project/history/execution/:executionId/test/:testId", - vulnerabilityCollectionView.create() to "$VULN/list/:params?", - createVulnerabilityView.create() to VULN_CREATE, - uploadVulnerabilityView.create() to VULN_UPLOAD, - vulnerabilityView.create() to "$VULNERABILITY_SINGLE/:identifier", - demoCollectionView.create() to DEMO, - userProfileView.create() to "$VULN_PROFILE/:name", - topRatingView.create() to VULN_TOP_RATING, - termsOfUsageView.create() to TERMS_OF_USE, - cookieTermsOfUse.create() to COOKIE, - thanksForRegistrationView.create() to THANKS_FOR_REGISTRATION, - cosvSchemaView.create() to VULN_COSV_SCHEMA, - - userSettingsView.create { - this.userInfoSetter = props.userInfoSetter - userInfo = props.userInfo - type = SETTINGS_PROFILE - } to SETTINGS_PROFILE, - - userSettingsView.create { - this.userInfoSetter = props.userInfoSetter - userInfo = props.userInfo - type = SETTINGS_EMAIL - } to SETTINGS_EMAIL, - - userSettingsView.create { - userInfo = props.userInfo - type = SETTINGS_TOKEN - } to SETTINGS_TOKEN, - - userSettingsView.create { - userInfo = props.userInfo - type = SETTINGS_ORGANIZATIONS - } to SETTINGS_ORGANIZATIONS, - - userSettingsView.create { - userInfo = props.userInfo - type = SETTINGS_DELETE - } to SETTINGS_DELETE, - - ).forEach { (view, route) -> - PathRoute { - this.element = view - this.path = "/$route" + val routeToUserProfileView: RouteObject? = userInfo?.name?.let { userName -> + jso { + path = "/$userName" + element = Navigate.create { + to = "/$this/$SETTINGS_PROFILE" } } + } - props.userInfo?.name?.run { - PathRoute { - path = "/$this" - element = Navigate.create { to = "/$this/$SETTINGS_PROFILE" } - } + val routeToManageOrganizationView: RouteObject = jso { + path = "/$MANAGE_ORGANIZATIONS" + element = when (userInfo.isSuperAdmin()) { + true -> OrganizationAdminView::class.react.create() + else -> fallbackElementWithoutRouterLink } + } - PathRoute { - path = "/$MANAGE_ORGANIZATIONS" - element = when (props.userInfo.isSuperAdmin()) { - true -> OrganizationAdminView::class.react.create() - else -> fallbackNode - } - } + val routeToFallbackView: RouteObject = jso { + path = "*" + element = fallbackElementWithRouterLink + } - PathRoute { - path = "*" - element = FallbackView::class.react.create { - bigText = "404" - smallText = "Page not found" - withRouterLink = true + return listOf( + saveWelcomeView.create { this.userInfo = userInfo } to SAVE, + vulnWelcomeView.create { this.userInfo = userInfo } to VULN, + sandboxView.create() to SANDBOX, + AboutUsView::class.react.create() to ABOUT_US, + createOrganizationView.create() to CREATE_ORGANIZATION, + registrationView.create { + this.userInfo = userInfo + this.userInfoSetter = userInfoSetter + } to REGISTRATION, + CollectionView::class.react.create { currentUserInfo = userInfo } to PROJECTS, + contestListView.create { currentUserInfo = userInfo } to CONTESTS, + fallbackElementWithRouterLink to ERROR_404, + banView.create { this.userInfo = userInfo } to BAN, + contestGlobalRatingView.create() to CONTESTS_GLOBAL_RATING, + contestView.create() to "$CONTESTS/:contestName", + createContestTemplateView.create() to CREATE_CONTESTS_TEMPLATE, + contestTemplateView.create() to "$CONTESTS_TEMPLATE/:id", + contestExecutionView.create() to "$CONTESTS/:contestName/:organizationName/:projectName", + awesomeBenchmarksView.create() to AWESOME_BENCHMARKS, + createProjectView.create() to "$CREATE_PROJECT/:organization?", + organizationView.create() to ":owner", + historyView.create() to ":owner/:name/history", + projectView.create() to ":owner/:name", + createProjectProblemView.create() to "project/:owner/:name/security/problems/new", + projectProblemView.create() to "project/:owner/:name/security/problems/:id", + executionView.create() to ":organization/:project/history/execution/:executionId", + demoView.create() to "$DEMO/:organizationName/:projectName", + cpgView.create() to "$DEMO/cpg", + testExecutionDetailsView.create() to "/:organization/:project/history/execution/:executionId/test/:testId", + vulnerabilityCollectionView.create() to "$VULN/list/:params?", + createVulnerabilityView.create() to VULN_CREATE, + uploadVulnerabilityView.create() to VULN_UPLOAD, + vulnerabilityView.create() to "$VULNERABILITY_SINGLE/:identifier", + demoCollectionView.create() to DEMO, + userProfileView.create() to "$VULN_PROFILE/:name", + topRatingView.create() to VULN_TOP_RATING, + termsOfUsageView.create() to TERMS_OF_USE, + cookieTermsOfUse.create() to COOKIE, + thanksForRegistrationView.create() to THANKS_FOR_REGISTRATION, + cosvSchemaView.create() to VULN_COSV_SCHEMA, + + userSettingsView.create { + this.userInfo = userInfo + this.userInfoSetter = userInfoSetter + type = SETTINGS_PROFILE + } to SETTINGS_PROFILE, + + userSettingsView.create { + this.userInfo = userInfo + this.userInfoSetter = userInfoSetter + type = SETTINGS_EMAIL + } to SETTINGS_EMAIL, + + userSettingsView.create { + this.userInfo = userInfo + type = SETTINGS_TOKEN + } to SETTINGS_TOKEN, + + userSettingsView.create { + this.userInfo = userInfo + type = SETTINGS_ORGANIZATIONS + } to SETTINGS_ORGANIZATIONS, + + userSettingsView.create { + this.userInfo = userInfo + type = SETTINGS_DELETE + } to SETTINGS_DELETE, + + ) + .map { (view, route) -> + jso { + path = "/$route" + element = view } } - } -} - -private val fallbackNode = FallbackView::class.react.create { - bigText = "404" - smallText = "Page not found" - withRouterLink = false + .let { routes -> + routeToUserProfileView?.let { routes + it } ?: routes + } + .plus(routeToManageOrganizationView) + .plus(routeToFallbackView) + .plus(indexRoute) + .toTypedArray() } - -/** - * @param view - * @return a view or a fallback of user info is null - */ -fun UserInfoAwareMutablePropsWithChildren.viewWithFallBack(view: ReactElement<*>) = this.userInfo?.name?.let { - view -} ?: fallbackNode diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/routing/MobileRouting.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/routing/MobileRouting.kt index 4aaff721b8..9928a1e2c8 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/routing/MobileRouting.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/routing/MobileRouting.kt @@ -7,26 +7,27 @@ package com.saveourtool.save.frontend.routing import com.saveourtool.save.frontend.components.mobile.AboutUsMobileView import com.saveourtool.save.frontend.components.mobile.saveWelcomeMobileView import com.saveourtool.save.validation.FrontendRoutes +import js.core.jso import react.FC import react.Props import react.create import react.react -import react.router.PathRoute -import react.router.Routes +import react.router.dom.createBrowserRouter /** * Just put a map: View -> Route URL to this list */ val mobileRoutes: FC = FC { - Routes { - listOf( - AboutUsMobileView::class.react.create() to FrontendRoutes.ABOUT_US, - saveWelcomeMobileView.create() to "*", - ).forEach { - PathRoute { - this.element = it.first - this.path = "/${it.second}" + createBrowserRouter( + arrayOf( + jso { + path = FrontendRoutes.ABOUT_US.path + element = AboutUsMobileView::class.react.create() + }, + jso { + path = "*" + element = saveWelcomeMobileView.create() } - } - } + ) + ) } diff --git a/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/BasicRoutingTest.kt b/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/BasicRoutingTest.kt index f7a16fde2f..947138174c 100644 --- a/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/BasicRoutingTest.kt +++ b/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/BasicRoutingTest.kt @@ -1,13 +1,19 @@ package com.saveourtool.save.frontend import com.saveourtool.save.frontend.externals.findByTextAndCast -import com.saveourtool.save.frontend.externals.i18next.initI18n import com.saveourtool.save.frontend.externals.render import com.saveourtool.save.frontend.externals.screen -import com.saveourtool.save.frontend.routing.basicRouting -import web.html.HTMLHeadingElement +import com.saveourtool.save.frontend.routing.createBasicRoutes +import com.saveourtool.save.frontend.utils.stubInitI18n +import com.saveourtool.save.info.UserInfo +import js.core.jso +import react.FC import react.create -import react.router.MemoryRouter +import react.router.Outlet +import web.html.HTMLHeadingElement +import react.router.createMemoryRouter +import react.router.dom.RouterProvider +import react.useState import kotlin.js.Promise import kotlin.test.* import kotlin.test.Test @@ -16,12 +22,24 @@ class BasicRoutingTest { @Test fun basicRoutingShouldRenderIndexViewTest(): Promise { // App uses `BrowserRouter`, while `MemoryRouter` should be used for tests. Thus, app cannot be rendered - render( - MemoryRouter.create { - initI18n() - basicRouting() + val routerProvider = FC { + stubInitI18n() + val (userInfo, userInfoSetter) = useState() + RouterProvider { + router = createMemoryRouter( + routes = arrayOf( + jso { + path = "/" + element = FC { + Outlet() + }.create() + children = createBasicRoutes(userInfo, userInfoSetter) + } + ) + ) } - ) + } + render(routerProvider.create()) screen.findByTextAndCast( "Cloud Platform for CI and Benchmarking of Code Analyzers" diff --git a/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/components/topbar/TopBarTest.kt b/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/components/topbar/TopBarTest.kt index e3e82fd798..1acc45515b 100644 --- a/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/components/topbar/TopBarTest.kt +++ b/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/components/topbar/TopBarTest.kt @@ -1,18 +1,20 @@ package com.saveourtool.save.frontend.components.topbar import com.saveourtool.save.frontend.externals.* +import com.saveourtool.save.frontend.utils.stubInitI18n import com.saveourtool.save.info.UserInfo import web.html.HTMLDivElement import web.html.HTMLSpanElement import react.* -import react.router.MemoryRouter import kotlin.test.* import js.core.jso +import react.router.createMemoryRouter +import react.router.dom.RouterProvider /** - * [MemoryRouter] is used to enable usage of `useLocation` hook inside the component + * [createMemoryRouter] is used to enable usage of `useLocation` hook inside the component * todo: functionality that is not covered * * How breadcrumbs are displayed * * `/execution` is trimmed from breadcrumbs @@ -21,16 +23,7 @@ import js.core.jso class TopBarTest { @Test fun topBarShouldRenderWithUserInfo() { - val rr = render( - MemoryRouter.create { - initialEntries = arrayOf( - "/" - ) - topBarComponent { - userInfo = UserInfo(name = "Test User") - } - } - ) + val rr = render(topBarComponentView(UserInfo(name = "Test User")).create()) val userInfoSpan: HTMLSpanElement? = screen.queryByTextAndCast("Test User") assertNotNull(userInfoSpan) @@ -45,16 +38,7 @@ class TopBarTest { @Test fun topBarShouldRenderWithoutUserInfo() { - val rr = render( - MemoryRouter.create { - initialEntries = arrayOf( - "/" - ) - topBarComponent { - userInfo = null - } - } - ) + val rr = render(topBarComponentView(null).create()) val userInfoSpan: HTMLSpanElement? = screen.queryByTextAndCast("Test User") assertNull(userInfoSpan) @@ -64,4 +48,22 @@ class TopBarTest { val dropdown = rr.container.querySelector("[aria-labelledby=\"userDropdown\"]") as HTMLDivElement assertEquals(1, dropdown.children.length, "When user is not logged in, dropdown menu should contain 1 entry") } + + companion object { + private fun topBarComponentView(userInfo: UserInfo?) = FC { + stubInitI18n() + RouterProvider { + router = createMemoryRouter( + routes = arrayOf( + jso { + index = true + element = FC { + topBarComponent { this.userInfo = userInfo } + }.create() + } + ) + ) + } + } + } } diff --git a/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/components/views/OrganizationViewTest.kt b/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/components/views/OrganizationViewTest.kt index 127861b3b9..c0b0950cd1 100644 --- a/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/components/views/OrganizationViewTest.kt +++ b/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/components/views/OrganizationViewTest.kt @@ -11,7 +11,6 @@ import kotlinx.datetime.LocalDateTime import react.create import react.react -import react.router.MemoryRouter import kotlin.js.Promise import kotlin.test.* diff --git a/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/components/views/ProjectViewTest.kt b/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/components/views/ProjectViewTest.kt index 49d37df5bf..9637a7a1ea 100644 --- a/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/components/views/ProjectViewTest.kt +++ b/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/components/views/ProjectViewTest.kt @@ -2,6 +2,7 @@ package com.saveourtool.save.frontend.components.views import com.saveourtool.save.domain.Role import com.saveourtool.save.entities.* +import com.saveourtool.save.entities.contest.ContestResult import com.saveourtool.save.frontend.externals.* import com.saveourtool.save.frontend.utils.apiUrl import com.saveourtool.save.frontend.utils.mockMswResponse @@ -17,6 +18,7 @@ import kotlin.test.* import js.core.jso class ProjectViewTest { + private val testOrganization = OrganizationDto.empty .copy( name = "TestOrg", @@ -47,6 +49,14 @@ class ProjectViewTest { ) } }, + rest.get("$apiUrl/projects/${testOrganization.name}/${testProject.name}/users") { _, res, _ -> + res { response -> + mockMswResponse( + response, + listOf(testUserInfo) + ) + } + }, rest.get("$apiUrl/projects/${testOrganization.name}/${testProject.name}/users/roles") { _, res, _ -> res { response -> mockMswResponse( @@ -71,6 +81,14 @@ class ProjectViewTest { ) } }, + rest.get("$apiUrl/contests/${testOrganization.name}/${testProject.name}/best") { _, res, _ -> + res { response -> + mockMswResponse( + response, + emptyList(), + ) + } + }, ) @Test diff --git a/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/utils/ServerSentEventTest.kt b/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/utils/ServerSentEventTest.kt index 8b8e24e0f7..5495e51ea9 100644 --- a/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/utils/ServerSentEventTest.kt +++ b/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/utils/ServerSentEventTest.kt @@ -11,7 +11,6 @@ import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlinx.browser.window -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json /** diff --git a/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/utils/TestUtils.kt b/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/utils/TestUtils.kt index edcf7c4a39..b73224264e 100644 --- a/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/utils/TestUtils.kt +++ b/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/utils/TestUtils.kt @@ -6,6 +6,7 @@ package com.saveourtool.save.frontend.utils import com.saveourtool.save.frontend.components.RequestStatusContext import com.saveourtool.save.frontend.components.requestStatusContext +import js.core.jso import org.w3c.fetch.Response import react.FC @@ -16,17 +17,32 @@ import web.timers.setTimeout import kotlin.js.Promise import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import react.router.MemoryRouter +import react.create +import react.router.createMemoryRouter +import react.router.dom.RouterProvider val wrapper: FC = FC { props -> + stubInitI18n() val (_, setMockState) = useState(null) val (_, setRedirectToFallbackView) = useState(false) val (_, setLoadingCounter) = useState(0) - MemoryRouter { - requestStatusContext.Provider { - value = RequestStatusContext(setMockState, setRedirectToFallbackView, setLoadingCounter) - +props.children - } + RouterProvider { + router = createMemoryRouter( + routes = arrayOf( + jso { + index = true + element = FC { + requestStatusContext.Provider { + value = RequestStatusContext(setMockState, setRedirectToFallbackView, setLoadingCounter) + +props.children + } + }.create() + } + ), + opts = jso { + initialEntries = arrayOf("/") + } + ) } } @@ -51,3 +67,22 @@ inline fun mockMswResponse(response: dynamic, value: T): dynamic { fun wait(millis: Int) = Promise { resolve, _ -> setTimeout({ resolve(Unit) }, millis) } + +/** + * Stub `initI18n` for testing purposes + */ +internal fun stubInitI18n() { + val i18n: dynamic = kotlinext.js.require("i18next"); + val reactI18n: dynamic = kotlinext.js.require("react-i18next"); + val i18nResources: dynamic = jso { + en = jso { + translation = undefined + } + } + + i18n.use(reactI18n.initReactI18next).init(jso { + resources = i18nResources + lng = "en" + fallbackLng = "en" + }) +} diff --git a/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/utils/UseRequestTest.kt b/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/utils/UseRequestTest.kt index 312c73d55f..3362d6c03d 100644 --- a/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/utils/UseRequestTest.kt +++ b/save-frontend/src/test/kotlin/com/saveourtool/save/frontend/utils/UseRequestTest.kt @@ -6,7 +6,6 @@ import com.saveourtool.save.frontend.externals.setupWorker import org.w3c.fetch.Headers import react.FC -import react.Props import react.create import react.useEffect import react.useState @@ -31,7 +30,7 @@ class UseRequestTest { @Test fun test(): Promise { val worker = createWorker() - val testComponent: FC = FC { + val testComponent = FC { val (sendSecond, setSendSecond) = useState(false) val (sendThird, setSendThird) = useState(false) useRequest(dependencies = arrayOf(sendSecond)) { diff --git a/yarn.lock b/yarn.lock index 0baaa9fdc7..ba0f125d42 100644 --- a/yarn.lock +++ b/yarn.lock @@ -454,10 +454,10 @@ dependencies: "@react-sigma/layout-core" "^3.1.0" -"@remix-run/router@1.10.0", "@remix-run/router@^1.9.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.10.0.tgz#e2170dc2049b06e65bbe883adad0e8ddf8291278" - integrity sha512-Lm+fYpMfZoEucJ7cMxgt4dYt8jLfbpwRCzAjm9UgSLOkmlqo9gupxt6YX3DY0Fk155NT9l17d/ydi+964uS9Lw== +"@remix-run/router@1.11.0", "@remix-run/router@^1.10.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.11.0.tgz#e0e45ac3fff9d8a126916f166809825537e9f955" + integrity sha512-BHdhcWgeiudl91HvVa2wxqZjSHbheSgIiDvxrF1VjFzBzpTtuDPkOdOi3Iqvc08kXtFkLjhbS+ML9aM8mJS+wQ== "@socket.io/base64-arraybuffer@~1.0.2": version "1.0.2" @@ -5266,20 +5266,20 @@ react-modal@^3.0.0: react-lifecycles-compat "^3.0.0" warning "^4.0.3" -react-router-dom@^6.16.0: - version "6.17.0" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.17.0.tgz#ea73f89186546c1cf72b10fcb7356d874321b2ad" - integrity sha512-qWHkkbXQX+6li0COUUPKAUkxjNNqPJuiBd27dVwQGDNsuFBdMbrS6UZ0CLYc4CsbdLYTckn4oB4tGDuPZpPhaQ== +react-router-dom@^6.17.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.18.0.tgz#0a50c167209d6e7bd2ed9de200a6579ea4fb1dca" + integrity sha512-Ubrue4+Ercc/BoDkFQfc6og5zRQ4A8YxSO3Knsne+eRbZ+IepAsK249XBH/XaFuOYOYr3L3r13CXTLvYt5JDjw== dependencies: - "@remix-run/router" "1.10.0" - react-router "6.17.0" + "@remix-run/router" "1.11.0" + react-router "6.18.0" -react-router@6.17.0, react-router@^6.16.0: - version "6.17.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.17.0.tgz#7b680c4cefbc425b57537eb9c73bedecbdc67c1e" - integrity sha512-YJR3OTJzi3zhqeJYADHANCGPUu9J+6fT5GLv82UWRGSxu6oJYCKVmxUcaBQuGm9udpWmPsvpme/CdHumqgsoaA== +react-router@6.18.0, react-router@^6.17.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.18.0.tgz#32e2bedc318e095a48763b5ed7758e54034cd36a" + integrity sha512-vk2y7Dsy8wI02eRRaRmOs9g2o+aE72YCx5q9VasT1N9v+lrdB79tIqrjMfByHiY5+6aYkH2rUa5X839nwWGPDg== dependencies: - "@remix-run/router" "1.10.0" + "@remix-run/router" "1.11.0" react-scroll-motion@^0.3.0: version "0.3.0"