diff --git a/src/frontend/delete.nim b/src/frontend/delete.nim index 789aebc..37446e5 100644 --- a/src/frontend/delete.nim +++ b/src/frontend/delete.nim @@ -94,7 +94,7 @@ when defined(js): proc render*(state: DeleteModal): VNode = result = buildHtml(): tdiv(class=class({"active": state.shown}, "modal modal-sm"), - id="login-modal"): + id="delete-modal"): a(href="", class="modal-overlay", "aria-label"="close", onClick=(ev: Event, n: VNode) => onClose(ev, n, state)) tdiv(class="modal-container"): @@ -122,11 +122,11 @@ when defined(js): button(class=class( {"loading": state.loading}, - "btn btn-primary" + "btn btn-primary delete-btn" ), onClick=(ev: Event, n: VNode) => onDelete(ev, n, state)): italic(class="fas fa-trash-alt") text " Delete" - button(class="btn", + button(class="btn cancel-btn", onClick=(ev: Event, n: VNode) => (state.shown = false)): - text "Cancel" \ No newline at end of file + text "Cancel" diff --git a/src/frontend/editbox.nim b/src/frontend/editbox.nim index 3f8753e..d461f90 100644 --- a/src/frontend/editbox.nim +++ b/src/frontend/editbox.nim @@ -87,7 +87,7 @@ when defined(js): text state.error.get().message tdiv(class="edit-buttons"): - tdiv(class="reply-button"): + tdiv(class="cancel-button"): button(class="btn btn-link", onClick=(e: Event, n: VNode) => (state.onEditCancel())): text " Cancel" @@ -95,4 +95,4 @@ when defined(js): button(class=class({"loading": state.loading}, "btn btn-primary"), onClick=(e: Event, n: VNode) => state.save()): italic(class="fas fa-check") - text " Save" \ No newline at end of file + text " Save" diff --git a/src/frontend/forum.nim b/src/frontend/forum.nim index cfc4151..4dea047 100644 --- a/src/frontend/forum.nim +++ b/src/frontend/forum.nim @@ -149,4 +149,4 @@ proc render(): VNode = ]) window.onPopState = onPopState -setRenderer render \ No newline at end of file +setRenderer render diff --git a/src/frontend/signup.nim b/src/frontend/signup.nim index 20a14ae..6a422d6 100644 --- a/src/frontend/signup.nim +++ b/src/frontend/signup.nim @@ -78,18 +78,19 @@ when defined(js): "data-sitekey"=recaptchaSiteKey.get()) script(src="https://www.google.com/recaptcha/api.js") tdiv(class="modal-footer"): - button(class=class({"loading": state.loading}, "btn btn-primary"), - onClick=(ev: Event, n: VNode) => onSignUpClick(ev, n, state)): + button(class=class({"loading": state.loading}, + "btn btn-primary create-account-btn"), + onClick=(ev: Event, n: VNode) => onSignUpClick(ev, n, state)): text "Create account" - button(class="btn", + button(class="btn login-btn", onClick=(ev: Event, n: VNode) => (state.onLogIn(); state.shown = false)): text "Log in" p(class="license-text text-gray"): text "By registering, you agree to the " - a(href=makeUri("/about/license"), + a(id="license", href=makeUri("/about/license"), onClick=(ev: Event, n: VNode) => (state.shown = false; anchorCB(ev, n))): text "content license" - text "." \ No newline at end of file + text "." diff --git a/src/frontend/usermenu.nim b/src/frontend/usermenu.nim index 65ea45c..89954a9 100644 --- a/src/frontend/usermenu.nim +++ b/src/frontend/usermenu.nim @@ -24,7 +24,7 @@ when defined(js): proc render*(state: UserMenu, user: User): VNode = result = buildHtml(): - tdiv(): + tdiv(id="profile-btn"): figure(class="avatar c-hand", onClick=(e: Event, n: VNode) => onClick(e, n, state)): img(src=user.avatarUrl, title=user.name) @@ -52,13 +52,15 @@ when defined(js): tdiv(class="tile-icon"): img(class="avatar", src=user.avatarUrl, title=user.name) - tdiv(class="tile-content"): + tdiv(id="profile-name", class="tile-content"): text user.name li(class="divider") li(class="menu-item"): - a(href=makeUri("/profile/" & user.name)): + a(id="myprofile-btn", + href=makeUri("/profile/" & user.name)): text "My profile" li(class="menu-item c-hand"): - a(onClick = (e: Event, n: VNode) => + a(id="logout-btn", + onClick = (e: Event, n: VNode) => (state.shown=false; state.onLogout())): - text "Logout" \ No newline at end of file + text "Logout" diff --git a/src/setup_nimforum.nim b/src/setup_nimforum.nim index a015f2d..3c5cf5b 100644 --- a/src/setup_nimforum.nim +++ b/src/setup_nimforum.nim @@ -22,10 +22,25 @@ proc backup(path: string, contents: Option[string]=none[string]()) = echo(path, " already exists. Moving to ", backupPath) moveFile(path, backupPath) +proc createUser(db: DbConn, user: tuple[username, password, email: string], + rank: Rank) = + assert user.username.len != 0 + let salt = makeSalt() + let password = makePassword(user.password, salt) + + exec(db, sql""" + INSERT INTO person(name, password, email, salt, status, lastOnline) + VALUES (?, ?, ?, ?, ?, DATETIME('now')) + """, user.username, password, user.email, salt, $rank) + proc initialiseDb(admin: tuple[username, password, email: string], filename="nimforum.db") = - let path = getCurrentDir() / filename - if "-dev" notin filename and "-test" notin filename: + let + path = getCurrentDir() / filename + isTest = "-test" in filename + isDev = "-dev" in filename + + if not isDev and not isTest: backup(path) removeFile(path) @@ -98,13 +113,16 @@ proc initialiseDb(admin: tuple[username, password, email: string], db.exec sql"create index PersonStatusIdx on person(status);" # Create default user. - if admin.username.len != 0: - let salt = makeSalt() - let password = makePassword(admin.password, salt) - db.exec(sql""" - insert into person (id, name, password, email, salt, status) - values (1, ?, ?, ?, ?, ?); - """, admin.username, password, admin.email, salt, $Admin) + db.createUser(admin, Admin) + + # Create test users if test or development + if isTest or isDev: + for rank in Spammer..Moderator: + let rankLower = toLowerAscii($rank) + let user = (username: $rankLower, + password: $rankLower, + email: $rankLower & "@localhost.local") + db.createUser(user, rank) # -- Post diff --git a/tests/browsertests/common.nim b/tests/browsertests/common.nim index 3d651e3..a2fb09b 100644 --- a/tests/browsertests/common.nim +++ b/tests/browsertests/common.nim @@ -1,5 +1,57 @@ -import os, options +import os, options, unittest import webdriver +import macros + +macro with*(obj: typed, code: untyped): untyped = + ## Execute a set of statements with an object + expectKind code, nnkStmtList + result = code + + # Simply inject obj into call + for i in 0 ..< result.len: + if result[i].kind in {nnkCommand, nnkCall}: + result[i].insert(1, obj) + +template click*(session: Session, element: string, strategy=CssSelector) = + let el = session.findElement(element, strategy) + check el.isSome() + el.get().click() + +template sendKeys*(session: Session, element, keys: string) = + let el = session.findElement(element) + check el.isSome() + el.get().sendKeys(keys) + +template sendKeys*(session: Session, element: string, keys: varargs[Key]) = + let el = session.findElement(element) + check el.isSome() + + # focus + el.get().click() + for key in keys: + session.press(key) + +template ensureExists*(session: Session, element: string) = + let el = session.findElement(element) + check el.isSome() + +template check*(session: Session, element: string, function: untyped) = + let el = session.findElement(element) + check function(el) + +template check*(session: Session, element: string, + strategy: LocationStrategy, function: untyped) = + let el = session.findElement(element, strategy) + check function(el) + +template checkIsNone*(session: Session, element: string, strategy=CssSelector) = + let el = session.findElement(element, strategy) + check el.isNone() + +template checkText*(session: Session, element, expectedValue: string) = + let el = session.findElement(element) + check el.isSome() + check el.get().getText() == expectedValue proc waitForLoad*(session: Session, timeout=20000) = var waitTime = 0 @@ -14,29 +66,23 @@ proc waitForLoad*(session: Session, timeout=20000) = if waitTime > timeout: doAssert false, "Wait for load time exceeded" +proc wait*(session: Session, msTimeout: int = 5000) = + session.waitForLoad(msTimeout) + proc logout*(session: Session) = - # Check whether we can log out. - let logoutLink = session.findElement( - "Logout", - LinkTextSelector - ).get() - logoutLink.click() + with session: + wait() + click "#profile-btn" + click "#profile-btn #logout-btn" + wait() proc login*(session: Session, user, password: string) = - let logIn = session.findElement("#login-btn").get() - logIn.click() + with session: + click "#login-btn" - let usernameField = session.findElement( - "#login-form input[name='username']" - ) + sendKeys "#login-form input[name='username']", user + sendKeys "#login-form input[name='password']", password - let passwordField = session.findElement( - "#login-form input[name='password']" - ) + sendKeys "#login-form input[name='password']", Key.Enter - usernameField.get().sendKeys("admin") - passwordField.get().sendKeys("admin") - passwordField.get().click() # Focus field. - session.press(Key.Enter) - - waitForLoad(session, 5000) \ No newline at end of file + wait() diff --git a/tests/browsertests/scenario1.nim b/tests/browsertests/scenario1.nim index fe8ef22..18d1bc7 100644 --- a/tests/browsertests/scenario1.nim +++ b/tests/browsertests/scenario1.nim @@ -9,96 +9,51 @@ proc test*(session: Session, baseUrl: string) = # Sanity checks test "shows sign up": - let signUp = session.findElement("#signup-btn") - check signUp.get().getText() == "Sign up" + session.checkText("#signup-btn", "Sign up") test "shows log in": - let logIn = session.findElement("#login-btn") - check logIn.get().getText() == "Log in" + session.checkText("#login-btn", "Log in") test "is empty": - let thread = session.findElement("tr > td.thread-title") - check thread.isNone() + session.checkIsNone("tr > td.thread-title") # Logging in test "can login/logout": - let logIn = session.findElement("#login-btn").get() - logIn.click() + with session: + click "#login-btn" - let usernameField = session.findElement( - "#login-form input[name='username']" - ) - check usernameField.isSome() - let passwordField = session.findElement( - "#login-form input[name='password']" - ) - check passwordField.isSome() + sendKeys "#login-form input[name='username']", "admin" + sendKeys "#login-form input[name='password']", "admin" - usernameField.get().sendKeys("admin") - passwordField.get().sendKeys("admin") - passwordField.get().click() # Focus field. - session.press(Key.Enter) + sendKeys "#login-form input[name='password']", Key.Enter + wait() - waitForLoad(session, 5000) + # Verify that the user menu has been initialised properly. + click "#profile-btn" + checkText "#profile-btn #profile-name", "admin" - # Verify that the user menu has been initialised properly. - let profileButton = session.findElement( - "#main-navbar figure.avatar" - ).get() - profileButton.click() - - let profileName = session.findElement( - "#main-navbar .menu-right div.tile-content" - ).get() - - check profileName.getText() == "admin" - - # Check whether we can log out. - let logoutLink = session.findElement( - "Logout", - LinkTextSelector - ).get() - logoutLink.click() - - # Verify we have logged out by looking for the log in button. - check session.findElement("#login-btn").isSome() + # Check whether we can log out. + click "#logout-btn" + # Verify we have logged out by looking for the log in button. + ensureExists "#login-btn" test "can register": - let signup = session.findElement("#signup-btn").get() - signup.click() + with session: + click "#signup-btn" - let emailField = session.findElement( - "#signup-form input[name='email']" - ).get() - let usernameField = session.findElement( - "#signup-form input[name='username']" - ).get() - let passwordField = session.findElement( - "#signup-form input[name='password']" - ).get() + sendKeys "#signup-form input[name='email']", "test@test.com" + sendKeys "#signup-form input[name='username']", "test" + sendKeys "#signup-form input[name='password']", "test" - emailField.sendKeys("test@test.com") - usernameField.sendKeys("test") - passwordField.sendKeys("test") + click "#signup-modal .create-account-btn" + wait() - let createAccount = session.findElement( - "#signup-modal .modal-footer .btn-primary" - ).get() + # Verify that the user menu has been initialised properly. + click "#profile-btn" + checkText "#profile-btn #profile-name", "test" + # close menu + click "#profile-btn" - createAccount.click() - - waitForLoad(session, 5000) - - # Verify that the user menu has been initialised properly. - let profileButton = session.findElement( - "#main-navbar figure.avatar" - ).get() - profileButton.click() - - let profileName = session.findElement( - "#main-navbar .menu-right div.tile-content" - ).get() - - check profileName.getText() == "test" - - logout(session) \ No newline at end of file + session.navigate(baseUrl) + session.wait() + logout(session) diff --git a/tests/browsertests/threads.nim b/tests/browsertests/threads.nim index 1474ec6..74729a3 100644 --- a/tests/browsertests/threads.nim +++ b/tests/browsertests/threads.nim @@ -3,44 +3,119 @@ import unittest, options, os, common import webdriver proc test*(session: Session, baseUrl: string) = - session.navigate(baseUrl) + let + userTitleStr = "This is a user thread!" + userContentStr = "A user has filled this out" - waitForLoad(session) + adminTitleStr = "This is a thread title!" + adminContentStr = "This is content" - login(session, "admin", "admin") + suite "user thread tests": + session.navigate(baseUrl) + session.wait() + login(session, "user", "user") - test "can create thread": - let newThreadBtn = session.findElement("#new-thread-btn").get() - newThreadBtn.click() + setup: + session.navigate(baseUrl) + session.wait() - waitForLoad(session) + test "can create thread": + with session: + click "#new-thread-btn" + wait() - let newThread = session.findElement("#new-thread") - check newThread.isSome() + sendKeys "#thread-title", userTitleStr + sendKeys "#reply-textarea", userContentStr - let createThreadBtn = session.findElement("#create-thread-btn") - check createThreadBtn.isSome() + click "#create-thread-btn" + wait() + checkText "#thread-title", userTitleStr + checkText ".original-post div.post-content", userContentStr - let threadTitle = session.findElement("#thread-title") - check threadTitle.isSome() + session.navigate(baseUrl) + session.wait() + logout(session) - let replyBox = session.findElement("#reply-textarea") - check replyBox.isSome() + suite "admin thread tests": + session.navigate(baseUrl) + session.wait() + login(session, "admin", "admin") - threadTitle.get().sendKeys("This is a thread title!") - replyBox.get().sendKeys("This is content.") + setup: + session.navigate(baseUrl) + session.wait() - createThreadBtn.get().click() + test "can create thread": + with session: + click "#new-thread-btn" + wait() - waitForLoad(session) + sendKeys "#thread-title", adminTitleStr + sendKeys "#reply-textarea", adminContentStr - let newThreadTitle = session.findElement("#thread-title") - check newThreadTitle.isSome() + click "#create-thread-btn" + wait() - check newThreadTitle.get().getText() == "This is a thread title!" + checkText "#thread-title", adminTitleStr + checkText ".original-post div.post-content", adminContentStr - let content = session.findElement(".original-post div.post-content") - check content.isSome() + test "try create duplicate thread": + with session: + click "#new-thread-btn" + wait() + ensureExists "#new-thread" - check content.get().getText() == "This is content." + sendKeys "#thread-title", adminTitleStr + sendKeys "#reply-textarea", adminContentStr + + click "#create-thread-btn" + + wait() + + ensureExists "#new-thread p.text-error" + + test "can edit post": + let modificationText = " and I edited it!" + with session: + click adminTitleStr, LinkTextSelector + wait() + + click ".post-buttons .edit-button" + wait() + + sendKeys ".original-post #reply-textarea", modificationText + click ".edit-buttons .save-button" + wait() + + checkText ".original-post div.post-content", adminContentStr & modificationText + + test "can like thread": + # Try to like the user thread above + + with session: + click userTitleStr, LinkTextSelector + wait() + + click ".post-buttons .like-button" + + checkText ".post-buttons .like-button .like-count", "1" + + test "can delete thread": + with session: + click adminTitleStr, LinkTextSelector + wait() + + click ".post-buttons .delete-button" + wait() + + # click delete confirmation + click "#delete-modal .delete-btn" + wait() + + # Make sure the forum post is gone + checkIsNone adminTitleStr, LinkTextSelector + + session.navigate(baseUrl) + session.wait() + logout(session)