diff --git a/README.md b/README.md index c8057cd..05d4667 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,24 @@ test Runs tester fasttest Runs tester without recompiling backend ``` +To get up and running: + +```bash +git clone https://github.com/nim-lang/nimforum +cd nimforum +git submodule update --init --recursive + +# Setup the db with user: admin, pass: admin and some other users +nimble devdb + +# Run this again if frontend code changes +nimble frontend + +# Will start a server at localhost:5000 +nimble backend +``` + + Development typically involves running `nimble devdb` which sets up the database for development and testing, then `nimble backend` which compiles and runs the forum's backend, and `nimble frontend` diff --git a/public/css/nimforum.scss b/public/css/nimforum.scss index 530180b..9842d8d 100644 --- a/public/css/nimforum.scss +++ b/public/css/nimforum.scss @@ -107,6 +107,24 @@ $logo-height: $navbar-height - 20px; } } +#category-selection { + .dropdown { + .btn { + margin-right: 0px; + } + } + .plus-btn { + margin-right: 0px; + i { + margin-right: 0px; + } + } +} + +.category { + white-space: nowrap; +} + #new-thread { .modal-container .modal-body { max-height: none; @@ -196,14 +214,12 @@ $threads-meta-color: #545d70; } } -.triangle { - // TODO: Abstract this into a "category" class. +.category-color { width: 0; height: 0; - border-left: 0.3rem solid transparent; - border-right: 0.3rem solid transparent; - border-bottom: 0.6rem solid #98c766; + border: 0.3rem solid #98c766; display: inline-block; + margin-right: 5px; } .load-more-separator { @@ -714,14 +730,4 @@ hr { // - Hide features that have not been implemented yet. #main-buttons > section.navbar-section:nth-child(1) { display: none; -} - -#threads-list.table { - tr > th:nth-child(2), tr > td:nth-child(2) { - display: none; - } -} - -.category, div.flag-button { - display: none; } \ No newline at end of file diff --git a/src/forum.nim b/src/forum.nim index 9ca8abf..c635f1a 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -527,10 +527,20 @@ proc updatePost(c: TForumData, postId: int, content: string, if row[0] == $postId: exec(db, crud(crUpdate, "thread", "name"), subject.get(), threadId) -proc executeNewThread(c: TForumData, subject, msg: string): (int64, int64) = +proc updateThread(c: TForumData, threadId: string, queryKeys: seq[string], queryValues: seq[string]) = + let threadAuthor = selectThreadAuthor(threadId.parseInt) + + # Verify that the current user has permissions to edit the specified thread. + let canEdit = c.rank == Admin or c.userid == threadAuthor.name + if not canEdit: + raise newForumError("You cannot edit this thread") + + exec(db, crud(crUpdate, "thread", queryKeys), queryValues) + +proc executeNewThread(c: TForumData, subject, msg, categoryID: string): (int64, int64) = const query = sql""" - insert into thread(name, views, modified) values (?, 0, DATETIME('now')) + insert into thread(name, views, modified, category) values (?, 0, DATETIME('now'), ?) """ assert c.loggedIn() @@ -550,13 +560,17 @@ proc executeNewThread(c: TForumData, subject, msg: string): (int64, int64) = if msg.len == 0: raise newForumError("Message is empty", @["msg"]) + let catID = getInt(categoryID, -1) + if catID == -1: + raise newForumError("CategoryID is invalid", @["categoryId"]) + if not validateRst(c, msg): raise newForumError("Message needs to be valid RST", @["msg"]) if rateLimitCheck(c): raise newForumError("You're posting too fast!") - result[0] = tryInsertID(db, query, subject).int + result[0] = tryInsertID(db, query, subject, categoryID).int if result[0] < 0: raise newForumError("Subject already exists", @["subject"]) @@ -658,6 +672,18 @@ proc executeLike(c: TForumData, postId: int) = # Save the like. exec(db, crud(crCreate, "like", "author", "post"), c.userid, postId) +proc executeNewCategory(c: TForumData, name, color, description: string): int64 = + + let canAdd = c.rank == Admin + + if not canAdd: + raise newForumError("You do not have permissions to add a category.") + + if name.len == 0: + raise newForumError("Category name must not be empty!", @["name"]) + + result = insertID(db, crud(crCreate, "category", "name", "color", "description"), name, color, description) + proc executeUnlike(c: TForumData, postId: int) = # Verify the post and like exists for the current user. const likeQuery = sql""" @@ -763,6 +789,19 @@ settings: routes: + get "/categories.json": + # TODO: Limit this query in the case of many many categories + const categoriesQuery = sql"""select * from category;""" + + var list = CategoryList(categories: @[]) + for data in getAllRows(db, categoriesQuery): + let category = Category( + id: data[0].getInt, name: data[1], description: data[2], color: data[3] + ) + list.categories.add(category) + + resp $(%list), "application/json" + get "/threads.json": var start = getInt(@"start", 0) @@ -1032,6 +1071,21 @@ routes: except ForumError as exc: resp Http400, $(%exc.data), "application/json" + post "/createCategory": + createTFD() + let formData = request.formData + + let name = formData["name"].body + let color = formData["color"].body.replace("#", "") + let description = formData["description"].body + + try: + let id = executeNewCategory(c, name, color, description) + let category = Category(id: id.int, name: name, color: color, description: description) + resp Http200, $(%category), "application/json" + except ForumError as exc: + resp Http400, $(%exc.data), "application/json" + get "/status.json": createTFD() @@ -1143,6 +1197,45 @@ routes: except ForumError as exc: resp Http400, $(%exc.data), "application/json" + post "/updateThread": + # TODO: Add some way of keeping track of modifications for historical + # purposes + createTFD() + if not c.loggedIn(): + let err = PostError( + errorFields: @[], + message: "Not logged in." + ) + resp Http401, $(%err), "application/json" + + let formData = request.formData + + cond "threadId" in formData + + let threadId = formData["threadId"].body + + # TODO: might want to add more properties here under a tighter permissions + # model + let keys = ["name", "category", "solution"] + + # optional parameters + var + queryValues: seq[string] = @[] + queryKeys: seq[string] = @[] + + for key in keys: + if key in formData: + queryKeys.add(key) + queryValues.add(formData[key].body) + + if queryKeys.len() > 0: + queryValues.add(threadId) + try: + updateThread(c, threadId, queryKeys, queryValues) + resp Http200, "{}", "application/json" + except ForumError as exc: + resp Http400, $(%exc.data), "application/json" + post "/newthread": createTFD() if not c.loggedIn(): @@ -1155,13 +1248,14 @@ routes: let formData = request.formData cond "msg" in formData cond "subject" in formData + cond "categoryId" in formData let msg = formData["msg"].body let subject = formData["subject"].body - # TODO: category + let categoryID = formData["categoryId"].body try: - let res = executeNewThread(c, subject, msg) + let res = executeNewThread(c, subject, msg, categoryID) resp Http200, $(%[res[0], res[1]]), "application/json" except ForumError as exc: resp Http400, $(%exc.data), "application/json" diff --git a/src/frontend/category.nim b/src/frontend/category.nim index 6c20f94..de2d248 100644 --- a/src/frontend/category.nim +++ b/src/frontend/category.nim @@ -6,6 +6,11 @@ type description*: string color*: string + CategoryList* = ref object + categories*: seq[Category] + +proc cmpNames*(cat1: Category, cat2: Category): int = + cat1.name.cmp(cat2.name) when defined(js): include karax/prelude @@ -17,12 +22,13 @@ when defined(js): result = buildHtml(): if category.name.len >= 0: tdiv(class="category", + title=category.description, "data-color"="#" & category.color): - tdiv(class="triangle", + tdiv(class="category-color", style=style( - (StyleAttr.borderBottom, - kstring"0.6rem solid #" & category.color) + (StyleAttr.border, + kstring"0.3rem solid #" & category.color) )) text category.name else: - span() \ No newline at end of file + span() diff --git a/src/frontend/categorypicker.nim b/src/frontend/categorypicker.nim new file mode 100644 index 0000000..0ea771f --- /dev/null +++ b/src/frontend/categorypicker.nim @@ -0,0 +1,193 @@ +when defined(js): + import sugar, httpcore, options, json, strutils, algorithm + import dom except Event + + include karax/prelude + import karax / [kajax, kdom, vstyles, vdom] + + import error, replybox, threadlist, post, category, user + import category, karaxutils + + type + CategoryPicker* = ref object of VComponent + list: Option[CategoryList] + selectedCategoryID*: int + loading: bool + modalShown: bool + addEnabled: bool + status: HttpCode + error: Option[PostError] + onCategoryChange: proc (oldCategory: Category, newCategory: Category) + onAddCategory: proc (category: Category) + + proc slug(name: string): string = + name.strip().replace(" ", "-").toLowerAscii + + proc onCategoryLoad(state: CategoryPicker): proc (httpStatus: int, response: kstring) = + return + proc (httpStatus: int, response: kstring) = + state.loading = false + state.status = httpStatus.HttpCode + if state.status != Http200: return + + let parsed = parseJson($response) + let list = parsed.to(CategoryList) + list.categories.sort(cmpNames) + + if state.list.isSome: + state.list.get().categories.add(list.categories) + else: + state.list = some(list) + + if state.selectedCategoryID > state.list.get().categories.len(): + state.selectedCategoryID = 0 + + proc loadCategories(state: CategoryPicker) = + if not state.loading: + state.loading = true + ajaxGet(makeUri("categories.json"), @[], onCategoryLoad(state)) + + proc `[]`*(state: CategoryPicker, id: int): Category = + for cat in state.list.get().categories: + if cat.id == id: + return cat + raise newException(IndexError, "Category at " & $id & " not found!") + + proc nullAddCategory(category: Category) = discard + proc nullCategoryChange(oldCategory: Category, newCategory: Category) = discard + + proc newCategoryPicker*( + onCategoryChange: proc(oldCategory: Category, newCategory: Category) = nullCategoryChange, + onAddCategory: proc(category: Category) = nullAddCategory + ): CategoryPicker = + result = CategoryPicker( + list: none[CategoryList](), + selectedCategoryID: 0, + loading: false, + modalShown: false, + addEnabled: false, + status: Http200, + error: none[PostError](), + onCategoryChange: onCategoryChange, + onAddCategory: onAddCategory + ) + + proc setAddEnabled*(state: CategoryPicker, enabled: bool) = + state.addEnabled = enabled + + proc select*(state: CategoryPicker, id: int) = + state.selectedCategoryID = id + state.markDirty() + + proc onCategoryClick(state: CategoryPicker, category: Category): proc (ev: Event, n: VNode) = + # this is necessary to capture the right value + let cat = category + return + proc (ev: Event, n: VNode) = + let oldCategory = state[state.selectedCategoryID] + state.select(cat.id) + state.onCategoryChange(oldCategory, cat) + + proc onAddCategoryPost(httpStatus: int, response: kstring, state: CategoryPicker) = + postFinished: + state.modalShown = false + let j = parseJson($response) + let category = j.to(Category) + + state.list.get().categories.add(category) + state.list.get().categories.sort(cmpNames) + state.select(category.id) + + state.onAddCategory(category) + + proc onAddCategoryClick(state: CategoryPicker) = + state.loading = true + state.error = none[PostError]() + + let uri = makeUri("createCategory") + let form = dom.document.getElementById("add-category-form") + let formData = newFormData(form) + + ajaxPost(uri, @[], cast[cstring](formData), + (s: int, r: kstring) => onAddCategoryPost(s, r, state)) + + proc onClose(ev: Event, n: VNode, state: CategoryPicker) = + state.modalShown = false + state.markDirty() + ev.preventDefault() + + proc genAddCategory(state: CategoryPicker): VNode = + result = buildHtml(): + tdiv(id="add-category"): + button(class="plus-btn btn btn-link", + onClick=(ev: Event, n: VNode) => ( + state.modalShown = true; + state.markDirty() + )): + italic(class="fas fa-plus") + tdiv(class=class({"active": state.modalShown}, "modal modal-sm")): + a(href="", class="modal-overlay", "aria-label"="close", + onClick=(ev: Event, n: VNode) => onClose(ev, n, state)) + tdiv(class="modal-container"): + tdiv(class="modal-header"): + tdiv(class="card-title h5"): + text "Add New Category" + tdiv(class="modal-body"): + form(id="add-category-form"): + genFormField( + state.error, "name", "Name", "text", false, + placeholder="Category Name") + genFormField( + state.error, "color", "Color", "color", false, + placeholder="#XXYYZZ" + ) + genFormField( + state.error, + "description", + "Description", + "text", + true, + placeholder="Description" + ) + tdiv(class="modal-footer"): + button( + id="add-category-btn", + class="btn btn-primary", + onClick=(ev: Event, n: VNode) => + state.onAddCategoryClick()): + text "Add" + + proc render*(state: CategoryPicker, currentUser: Option[User]): VNode = + let loggedIn = currentUser.isSome() + let currentAdmin = + loggedIn and currentUser.get().rank == Admin + + if currentAdmin: + state.setAddEnabled(true) + + if state.status != Http200: + return renderError("Couldn't retrieve categories.", state.status) + + if state.list.isNone: + state.loadCategories() + return buildHtml(tdiv(class="loading loading-lg")) + + let list = state.list.get().categories + let selectedCategory = state[state.selectedCategoryID] + + result = buildHtml(): + tdiv(id="category-selection", class="input-group"): + tdiv(class="dropdown"): + a(class="btn btn-link dropdown-toggle", tabindex="0"): + tdiv(class="selected-category d-inline-block"): + render(selectedCategory) + text " " + italic(class="fas fa-caret-down") + ul(class="menu"): + for category in list: + li(class="menu-item"): + a(class="category-" & $category.id & " " & category.name.slug, + onClick=onCategoryClick(state, category)): + render(category) + if state.addEnabled: + genAddCategory(state) diff --git a/src/frontend/forum.nim b/src/frontend/forum.nim index 5fb11b4..efb072e 100644 --- a/src/frontend/forum.nim +++ b/src/frontend/forum.nim @@ -83,7 +83,7 @@ proc render(): VNode = route([ r("/newthread", (params: Params) => - (render(state.newThread)) + (render(state.newThread, getLoggedInUser())) ), r("/profile/@username", (params: Params) => diff --git a/src/frontend/newthread.nim b/src/frontend/newthread.nim index e75b6bb..fe4e619 100644 --- a/src/frontend/newthread.nim +++ b/src/frontend/newthread.nim @@ -1,12 +1,13 @@ when defined(js): import sugar, httpcore, options, json import dom except Event + import jsffi except `&` include karax/prelude import karax / [kajax, kdom] - import error, replybox, threadlist, post - import karaxutils + import error, replybox, threadlist, post, category, user + import karaxutils, categorypicker type NewThread* = ref object @@ -14,11 +15,13 @@ when defined(js): error: Option[PostError] replyBox: ReplyBox subject: kstring + categoryPicker: CategoryPicker proc newNewThread*(): NewThread = NewThread( replyBox: newReplyBox(nil), - subject: "" + subject: "", + categoryPicker: newCategoryPicker() ) proc onSubjectChange(e: Event, n: VNode, state: NewThread) = @@ -37,12 +40,16 @@ when defined(js): let uri = makeUri("newthread") # TODO: This is a hack, karax should support this. let formData = newFormData() + let categoryID = state.categoryPicker.selectedCategoryID + formData.append("subject", state.subject) formData.append("msg", state.replyBox.getText()) - ajaxPost(uri, @[], cast[cstring](formData), + formData.append("categoryId", $categoryID) + + ajaxPost(uri, @[], formData.to(cstring), (s: int, r: kstring) => onCreatePost(s, r, state)) - proc render*(state: NewThread): VNode = + proc render*(state: NewThread, currentUser: Option[User]): VNode = result = buildHtml(): section(class="container grid-xl"): tdiv(id="new-thread"): @@ -55,6 +62,10 @@ when defined(js): if state.error.isSome(): p(class="text-error"): text state.error.get().message + tdiv(): + label(class="d-inline-block form-label"): + text "Category" + render(state.categoryPicker, currentUser) renderContent(state.replyBox, none[Thread](), none[Post]()) tdiv(class="footer"): diff --git a/src/frontend/postlist.nim b/src/frontend/postlist.nim index 6a27df3..46dee9a 100644 --- a/src/frontend/postlist.nim +++ b/src/frontend/postlist.nim @@ -15,17 +15,20 @@ type when defined(js): from dom import document + import jsffi except `&` include karax/prelude import karax / [vstyles, kajax, kdom] import karaxutils, error, replybox, editbox, postbutton, delete + import categorypicker type State = ref object list: Option[PostList] loading: bool status: HttpCode + error: Option[PostError] replyingTo: Option[Post] replyBox: ReplyBox editing: Option[Post] ## If in edit mode, this contains the post. @@ -33,8 +36,10 @@ when defined(js): likeButton: LikeButton deleteModal: DeleteModal lockButton: LockButton + categoryPicker: CategoryPicker proc onReplyPosted(id: int) + proc onCategoryChanged(oldCategory: Category, newCategory: Category) proc onEditPosted(id: int, content: string, subject: Option[string]) proc onEditCancelled() proc onDeletePost(post: Post) @@ -44,17 +49,37 @@ when defined(js): list: none[PostList](), loading: false, status: Http200, + error: none[PostError](), replyingTo: none[Post](), replyBox: newReplyBox(onReplyPosted), editBox: newEditBox(onEditPosted, onEditCancelled), likeButton: newLikeButton(), deleteModal: newDeleteModal(onDeletePost, onDeleteThread, nil), - lockButton: newLockButton() + lockButton: newLockButton(), + categoryPicker: newCategoryPicker(onCategoryChanged) ) var state = newState() + proc onCategoryPost(httpStatus: int, response: kstring, state: State) = + state.loading = false + postFinished: + discard + # TODO: show success message + + proc onCategoryChanged(oldCategory: Category, newCategory: Category) = + let uri = makeUri("/updateThread") + + let formData = newFormData() + formData.append("threadId", $state.list.get().thread.id) + formData.append("category", $newCategory.id) + + state.loading = true + + ajaxPost(uri, @[], formData.to(cstring), + (s: int, r: kstring) => onCategoryPost(s, r, state)) + proc onPostList(httpStatus: int, response: kstring, postId: Option[int]) = state.loading = false state.status = httpStatus.HttpCode @@ -66,6 +91,7 @@ when defined(js): state.list = some(list) dom.document.title = list.thread.topic & " - " & dom.document.title + state.categoryPicker.select(list.thread.category.id) # The anchor should be jumped to once all the posts have been loaded. if postId.isSome(): @@ -179,6 +205,20 @@ when defined(js): span(class="more-post-count"): text "(" & $post.moreBefore.len & ")" + proc genCategories(thread: Thread, currentUser: Option[User]): VNode = + let loggedIn = currentUser.isSome() + let authoredByUser = + loggedIn and currentUser.get().name == thread.author.name + let currentAdmin = + currentUser.isSome() and currentUser.get().rank == Admin + + result = buildHtml(): + tdiv(): + if authoredByUser or currentAdmin: + render(state.categoryPicker, currentUser) + else: + render(thread.category) + proc genPostButtons(post: Post, currentUser: Option[User]): Vnode = let loggedIn = currentUser.isSome() let authoredByUser = @@ -330,7 +370,10 @@ when defined(js): result = buildHtml(): section(class="container grid-xl"): tdiv(id="thread-title", class="title"): - p(): text list.thread.topic + if state.error.isSome(): + span(class="text-error"): + text state.error.get().message + p(class="title-text"): text list.thread.topic if list.thread.isLocked: italic(class="fas fa-lock fa-xs", title="Thread cannot be replied to") @@ -343,7 +386,7 @@ when defined(js): italic(class="fas fa-check-square fa-xs", title="Thread has a solution") text "Solved" - render(list.thread.category) + genCategories(list.thread, currentUser) tdiv(class="posts"): var prevPost: Option[Post] = none[Post]() for i, post in list.posts: diff --git a/src/setup_nimforum.nim b/src/setup_nimforum.nim index 5647e48..b235415 100644 --- a/src/setup_nimforum.nim +++ b/src/setup_nimforum.nim @@ -66,7 +66,7 @@ proc initialiseDb(admin: tuple[username, password, email: string], db.exec(sql""" insert into category (id, name, description, color) - values (0, 'Default', '', ''); + values (0, 'Default', 'The default category', ''); """) # -- Thread @@ -115,7 +115,7 @@ proc initialiseDb(admin: tuple[username, password, email: string], # Create default user. db.createUser(admin, Admin) - # Create test users if test or development + # Create some test data for development if isTest or isDev: for rank in Spammer..Moderator: let rankLower = toLowerAscii($rank) @@ -124,6 +124,14 @@ proc initialiseDb(admin: tuple[username, password, email: string], email: $rankLower & "@localhost.local") db.createUser(user, rank) + db.exec(sql""" + insert into category (name, description, color) + values ('Libraries', 'Libraries and library development', '0198E1'), + ('Announcements', 'Announcements by Nim core devs', 'FFEB3B'), + ('Fun', 'Posts that are just for fun', '00897B'), + ('Potential Issues', 'Potential Nim compiler issues', 'E53935'); + """) + # -- Post db.exec(sql""" diff --git a/tests/browsertester.nim b/tests/browsertester.nim index 4e03081..6a5c374 100644 --- a/tests/browsertester.nim +++ b/tests/browsertester.nim @@ -43,7 +43,7 @@ template withBackend(body: untyped): untyped = body -import browsertests/[scenario1, threads, issue181] +import browsertests/[scenario1, threads, issue181, categories] proc main() = spawn runProcess("geckodriver -p 4444 --log config") @@ -64,6 +64,7 @@ proc main() = withBackend: scenario1.test(session, baseUrl) threads.test(session, baseUrl) + categories.test(session, baseUrl) issue181.test(session, baseUrl) session.close() diff --git a/tests/browsertests/categories.nim b/tests/browsertests/categories.nim new file mode 100644 index 0000000..7924ac4 --- /dev/null +++ b/tests/browsertests/categories.nim @@ -0,0 +1,105 @@ +import unittest, options, os, common + +import webdriver + +proc selectCategory(session: Session, name: string) = + with session: + click "#category-selection .dropdown-toggle" + + click "#category-selection ." & name + + +proc categoriesUserTests(session: Session, baseUrl: string) = + let + title = "Category Test" + content = "Choosing category test" + + suite "user tests": + + with session: + navigate baseUrl + wait() + login "user", "user" + + setup: + with session: + navigate baseUrl + wait() + + test "no category add available": + with session: + click "#new-thread-btn" + wait() + + checkIsNone "#add-category" + + test "can create category thread": + with session: + click "#new-thread-btn" + wait() + + sendKeys "#thread-title", title + + selectCategory "fun" + + sendKeys "#reply-textarea", content + + click "#create-thread-btn" + wait() + + checkText "#thread-title .category", "Fun" + + navigate baseUrl + wait() + + ensureExists title, LinkTextSelector + + session.logout() + +proc categoriesAdminTests(session: Session, baseUrl: string) = + let + name = "Category Test" + color = "Creating category test" + description = "This is a description" + + suite "admin tests": + with session: + navigate baseUrl + wait() + login "admin", "admin" + + test "can create category": + with session: + click "#new-thread-btn" + wait() + + ensureExists "#add-category" + + click "#add-category .plus-btn" + wait() + + clear "#add-category input[name='name']" + clear "#add-category input[name='color']" + clear "#add-category input[name='description']" + + + sendKeys "#add-category input[name='name']", name + sendKeys "#add-category input[name='color']", color + sendKeys "#add-category input[name='description']", description + + click "#add-category #add-category-btn" + wait() + + checkText "#category-selection .selected-category", name + + session.logout() + +proc test*(session: Session, baseUrl: string) = + session.navigate(baseUrl) + session.wait() + + categoriesUserTests(session, baseUrl) + categoriesAdminTests(session, baseUrl) + + session.navigate(baseUrl) + session.wait() \ No newline at end of file diff --git a/tests/browsertests/common.nim b/tests/browsertests/common.nim index a2c9b51..02236ca 100644 --- a/tests/browsertests/common.nim +++ b/tests/browsertests/common.nim @@ -149,5 +149,5 @@ proc createThread*(session: Session, title, content: string) = click "#create-thread-btn" wait() - checkText "#thread-title", title + checkText "#thread-title .title-text", title checkText ".original-post div.post-content", content diff --git a/tests/browsertests/threads.nim b/tests/browsertests/threads.nim index f0b1b32..45b971b 100644 --- a/tests/browsertests/threads.nim +++ b/tests/browsertests/threads.nim @@ -15,6 +15,12 @@ proc banUser(session: Session, baseUrl: string) = setUserRank baseUrl, "user", "banned" logout() +proc unBanUser(session: Session, baseUrl: string) = + with session: + login "admin", "admin" + setUserRank baseUrl, "user", "user" + logout() + proc userTests(session: Session, baseUrl: string) = suite "user thread tests": session.login("user", "user") @@ -34,7 +40,7 @@ proc userTests(session: Session, baseUrl: string) = click "#create-thread-btn" wait() - checkText "#thread-title", userTitleStr + checkText "#thread-title .title-text", userTitleStr checkText ".original-post div.post-content", userContentStr session.logout() @@ -99,7 +105,7 @@ proc adminTests(session: Session, baseUrl: string) = click "#create-thread-btn" wait() - checkText "#thread-title", adminTitleStr + checkText "#thread-title .title-text", adminTitleStr checkText ".original-post div.post-content", adminContentStr test "try create duplicate thread": @@ -172,5 +178,7 @@ proc test*(session: Session, baseUrl: string) = anonymousTests(session, baseUrl) adminTests(session, baseUrl) + unBanUser(session, baseUrl) + session.navigate(baseUrl) session.wait()