Create Todo app
Summary: Ref T2687 Reviewers: #testers, kuba-orlik Reviewed By: #testers, kuba-orlik Subscribers: kuba-orlik, jenkins-user Maniphest Tasks: T2687 Differential Revision: https://hub.sealcode.org/D1339master
parent
b7cc271d97
commit
0c49effa9d
@ -0,0 +1,9 @@
|
||||
import { LONG_TEST_TIMEOUT } from "./src/back/test_utils/webhint";
|
||||
import { closeBrowser } from "./src/back/test_utils/browser-creator";
|
||||
|
||||
exports.mochaHooks = {
|
||||
async afterAll() {
|
||||
this.timeout(LONG_TEST_TIMEOUT);
|
||||
await closeBrowser();
|
||||
},
|
||||
};
|
@ -1,12 +1,14 @@
|
||||
#!/usr/bin/env -S bash -x
|
||||
|
||||
|
||||
# the "--no-TTY" option is crucial - without it the output is not captured in Jenkins
|
||||
|
||||
docker-compose run \
|
||||
--rm \
|
||||
--service-ports \
|
||||
-e "SEALIOUS_MONGO_PORT=27017" \
|
||||
-e "SEALIOUS_MONGO_HOST=db" \
|
||||
test \
|
||||
npm "$@"
|
||||
CONTAINER_ID=$(docker-compose run \
|
||||
-d \
|
||||
--service-ports \
|
||||
-e "SEALIOUS_MONGO_PORT=27017" \
|
||||
-e "SEALIOUS_MONGO_HOST=db" \
|
||||
test \
|
||||
npm "$@")
|
||||
|
||||
docker logs -f $CONTAINER_ID
|
||||
docker rm $CONTAINER_ID
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,21 @@
|
||||
import { Collection, FieldTypes, Policies } from "sealious";
|
||||
|
||||
export default class Tasks extends Collection {
|
||||
fields = {
|
||||
title: new FieldTypes.Text(),
|
||||
done: new (class extends FieldTypes.Boolean {
|
||||
hasDefaultValue = () => true;
|
||||
async getDefaultValue() {
|
||||
return false;
|
||||
}
|
||||
})(),
|
||||
};
|
||||
|
||||
policies = {
|
||||
create: new Policies.Public(),
|
||||
show: new Policies.Owner(),
|
||||
list: new Policies.Owner(),
|
||||
};
|
||||
|
||||
defaultPolicy = new Policies.Public();
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
const ADMIN_CREDENTIALS = {
|
||||
username: "admin",
|
||||
password: "adminadmin",
|
||||
email: "admin@example.com",
|
||||
};
|
||||
|
||||
export default ADMIN_CREDENTIALS;
|
@ -0,0 +1,53 @@
|
||||
import { BaseContext } from "koa";
|
||||
import { CollectionItem } from "sealious";
|
||||
import frame from "../../frame";
|
||||
import { Tasks } from "../../collections/collections";
|
||||
|
||||
export function Task(task: CollectionItem<typeof Tasks>) {
|
||||
return frame(
|
||||
`task-${task.id}`,
|
||||
/* HTML */ `<li class="task">
|
||||
<input
|
||||
type="checkbox"
|
||||
data-controller="task"
|
||||
data-action="task#toggle"
|
||||
data-id="${task.id}"
|
||||
${task.get("done") ? "checked" : ""}
|
||||
/>
|
||||
${task.get("title") as string}
|
||||
<form method="POST" action="/todo/">
|
||||
<input class="delete-button" type="submit" value="Delete" />
|
||||
<input
|
||||
class="hidden-button"
|
||||
type="hidden"
|
||||
name="taskId"
|
||||
value="${task.id}"
|
||||
/>
|
||||
<input
|
||||
class="hidden-button"
|
||||
type="hidden"
|
||||
id="action"
|
||||
name="action"
|
||||
value="delete"
|
||||
/>
|
||||
</form>
|
||||
</li>`
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
<form method="DELETE" action="/todo/${task.id}">
|
||||
<input class="delete-button" type="submit" value="Delete" />
|
||||
</form>
|
||||
*/
|
||||
|
||||
export async function TaskList(ctx: BaseContext) {
|
||||
const { items: tasks } = await ctx.$app.collections.tasks.list(ctx.$context).fetch();
|
||||
|
||||
const tasksTemplate = tasks.map(Task).join("\n");
|
||||
return `
|
||||
<ul>
|
||||
${tasksTemplate}
|
||||
</ul>
|
||||
`;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import { Context } from "koa";
|
||||
import { Mountable } from "@sealcode/sealgen";
|
||||
import Router from "@koa/router";
|
||||
|
||||
export const actionName = "Logout";
|
||||
|
||||
export default new (class LogoutRedirect extends Mountable {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async canAccess(_: Context) {
|
||||
return { canAccess: true, message: "" };
|
||||
}
|
||||
|
||||
mount(router: Router, path: string) {
|
||||
router.get(path, async (ctx) => {
|
||||
try {
|
||||
const session_id: string = ctx.cookies.get("sealious-session") as string;
|
||||
if (session_id) {
|
||||
await ctx.$app.collections.sessions.logout(
|
||||
new ctx.$app.SuperContext(),
|
||||
session_id
|
||||
);
|
||||
ctx.status = 302;
|
||||
ctx.redirect("/");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during logout:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
@ -0,0 +1,55 @@
|
||||
import assert from "assert";
|
||||
import { withProdApp } from "../test_utils/with-prod-app";
|
||||
import { LONG_TEST_TIMEOUT, VERY_LONG_TEST_TIMEOUT } from "../test_utils/webhint";
|
||||
import { LogoutURL, SignInURL } from "./urls";
|
||||
import { Browser, BrowserContext, Page } from "@playwright/test";
|
||||
import { getBrowser } from "../test_utils/browser-creator";
|
||||
import ADMIN_CREDENTIALS from "../default-admin-credentials";
|
||||
|
||||
describe("Logout", () => {
|
||||
let page: Page;
|
||||
let browser: Browser;
|
||||
let context: BrowserContext;
|
||||
const username = ADMIN_CREDENTIALS.username;
|
||||
const password = ADMIN_CREDENTIALS.password;
|
||||
|
||||
beforeEach(async () => {
|
||||
browser = await getBrowser();
|
||||
context = await browser.newContext();
|
||||
page = await context.newPage();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await context.close();
|
||||
});
|
||||
|
||||
it("doesn't crash", async function () {
|
||||
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||
return withProdApp(async ({ rest_api }) => {
|
||||
await assert.rejects(
|
||||
async () => {
|
||||
await rest_api.get(LogoutURL);
|
||||
},
|
||||
{ name: "Error" }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("logout test", () => {
|
||||
it("logout", async function () {
|
||||
await withProdApp(async ({ base_url }) => {
|
||||
this.timeout(LONG_TEST_TIMEOUT);
|
||||
await page.goto(base_url);
|
||||
await page.getByRole("link", { name: "Sign in" }).click();
|
||||
await page.getByPlaceholder("text").click();
|
||||
await page.getByPlaceholder("text").fill(username);
|
||||
await page.getByPlaceholder("text").press("Tab");
|
||||
await page.getByPlaceholder("password").fill(password);
|
||||
await page.getByPlaceholder("password").press("Enter");
|
||||
await page.waitForSelector(`a[href="${LogoutURL}"]`);
|
||||
await page.getByRole("link", { name: "Logout" }).click();
|
||||
await page.waitForSelector(`a[href="${SignInURL}"]`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,121 @@
|
||||
import { Context } from "koa";
|
||||
import {
|
||||
Form,
|
||||
FormData,
|
||||
FormDataValue,
|
||||
Fields,
|
||||
Controls,
|
||||
FormReaction,
|
||||
} from "@sealcode/sealgen";
|
||||
import html from "../html";
|
||||
import { Users } from "../collections/collections";
|
||||
import { FlatTemplatable, tempstream } from "tempstream";
|
||||
import { PageErrorMessage } from "@sealcode/sealgen/@types/page/mountable-with-fields";
|
||||
|
||||
export const actionName = "SignIn";
|
||||
|
||||
const fields = {
|
||||
username: new Fields.SimpleFormField(true),
|
||||
password: new Fields.SimpleFormField(true),
|
||||
};
|
||||
|
||||
export const SignInShape = Fields.fieldsToShape(fields);
|
||||
|
||||
export default new (class SignInForm extends Form<typeof fields, void> {
|
||||
defaultSuccessMessage = "Formularz wypełniony poprawnie";
|
||||
fields = fields;
|
||||
|
||||
controls = [
|
||||
new Controls.SimpleInput(fields.username, { label: "Username:", type: "text" }),
|
||||
new Controls.SimpleInput(fields.password, {
|
||||
label: "Password:",
|
||||
type: "password",
|
||||
}),
|
||||
];
|
||||
|
||||
async validateValues(
|
||||
ctx: Context,
|
||||
data: Record<string, FormDataValue>
|
||||
): Promise<{ valid: boolean; error: string }> {
|
||||
const { parsed: username } = await this.fields.username.getValue(ctx, data);
|
||||
|
||||
const filter: object = typeof username === "string" ? { username } : {};
|
||||
|
||||
const user = await Users.suList().filter(filter).fetch();
|
||||
|
||||
if (user.empty) {
|
||||
return { valid: false, error: `Incorrect password or username` };
|
||||
}
|
||||
|
||||
return { valid: true, error: `` };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async canAccess(ctx: Context) {
|
||||
if (ctx.$context.session_id) {
|
||||
return { canAccess: false, message: "" };
|
||||
}
|
||||
return { canAccess: true, message: "" };
|
||||
}
|
||||
|
||||
async onSuccess(
|
||||
_: Context,
|
||||
__: FormData<string>,
|
||||
_submitResult: void
|
||||
): Promise<FormReaction> {
|
||||
const reaction: FormReaction = {
|
||||
action: "redirect",
|
||||
url: "/",
|
||||
};
|
||||
console.log("Successfully logged in.");
|
||||
return reaction;
|
||||
}
|
||||
|
||||
async onError(
|
||||
ctx: Context,
|
||||
data: FormData<string>,
|
||||
error: unknown
|
||||
): Promise<FormReaction> {
|
||||
const reaction: FormReaction = {
|
||||
action: "stay",
|
||||
content: this.render(ctx, data, true),
|
||||
messages: [
|
||||
{
|
||||
type: "error",
|
||||
text: `There was an error while logging in: ${String(error)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
return reaction;
|
||||
}
|
||||
|
||||
async onSubmit(ctx: Context, data: FormData) {
|
||||
try {
|
||||
const sessionId: string = await Users.app.collections.sessions.login(
|
||||
data.raw_values.username as string,
|
||||
data.raw_values.password as string
|
||||
);
|
||||
|
||||
ctx.cookies.set("sealious-session", sessionId, {
|
||||
maxAge: 1000 * 60 * 60 * 24 * 7,
|
||||
secure: ctx.request.protocol === "https",
|
||||
overwrite: true,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(String(error));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async renderError(ctx: Context, error: PageErrorMessage): Promise<FlatTemplatable> {
|
||||
return html(ctx, "SignIn", `${error.message}`);
|
||||
}
|
||||
|
||||
async render(ctx: Context, data: FormData, show_field_errors: boolean) {
|
||||
return html(
|
||||
ctx,
|
||||
"SignIn",
|
||||
tempstream`${await super.render(ctx, data, show_field_errors)}`
|
||||
);
|
||||
}
|
||||
})();
|
@ -0,0 +1,103 @@
|
||||
import { withProdApp } from "../test_utils/with-prod-app";
|
||||
import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint";
|
||||
import { SignInURL, LogoutURL } from "./urls";
|
||||
import { Browser, BrowserContext, Page } from "@playwright/test";
|
||||
import { getBrowser } from "../test_utils/browser-creator";
|
||||
import ADMIN_CREDENTIALS from "../default-admin-credentials";
|
||||
|
||||
describe("SignIn", () => {
|
||||
let page: Page;
|
||||
let browser: Browser;
|
||||
let context: BrowserContext;
|
||||
const username = ADMIN_CREDENTIALS.username;
|
||||
const password = ADMIN_CREDENTIALS.password;
|
||||
|
||||
beforeEach(async () => {
|
||||
browser = await getBrowser();
|
||||
context = await browser.newContext();
|
||||
page = await context.newPage();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await context.close();
|
||||
});
|
||||
|
||||
it("doesn't crash", async function () {
|
||||
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||
return withProdApp(async ({ base_url, rest_api }) => {
|
||||
await rest_api.get(SignInURL);
|
||||
await webhintURL(base_url + SignInURL);
|
||||
// alternatively you can use webhintHTML for faster but less precise scans
|
||||
// or for scanning responses of requests that use some form of authorization:
|
||||
// const response = await rest_api.get(SignInURL);
|
||||
// await webhintHTML(response);
|
||||
});
|
||||
});
|
||||
|
||||
describe("can access test", () => {
|
||||
it("access url", async function () {
|
||||
await withProdApp(async ({ base_url }) => {
|
||||
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||
await page.goto(base_url);
|
||||
await page.getByRole("link", { name: "Sign in" }).click();
|
||||
await page.getByPlaceholder("text").click();
|
||||
await page.getByPlaceholder("text").fill(username);
|
||||
await page.getByPlaceholder("text").press("Tab");
|
||||
await page.getByPlaceholder("password").fill(password);
|
||||
await page.getByPlaceholder("password").press("Enter");
|
||||
await page.waitForSelector(`a[href="${LogoutURL}"]`);
|
||||
await page.goto(base_url + SignInURL);
|
||||
await page.waitForSelector('body:has-text("no access")');
|
||||
await page.goto(base_url);
|
||||
await page.getByRole("link", { name: "Logout" }).click();
|
||||
await page.waitForSelector(`a[href="${SignInURL}"]`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("sign in test", () => {
|
||||
it("wrong username", async function () {
|
||||
await withProdApp(async ({ base_url }) => {
|
||||
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||
await page.goto(base_url);
|
||||
await page.getByRole("link", { name: "Sign in" }).click();
|
||||
await page.getByPlaceholder("text").click();
|
||||
await page.getByPlaceholder("text").fill("username20230720722");
|
||||
await page.getByPlaceholder("text").press("Tab");
|
||||
await page.getByPlaceholder("password").fill("test");
|
||||
await page.getByPlaceholder("password").press("Enter");
|
||||
await page.waitForSelector(".form-message");
|
||||
});
|
||||
});
|
||||
|
||||
it("correct username and password", async function () {
|
||||
await withProdApp(async ({ base_url }) => {
|
||||
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||
await page.goto(base_url);
|
||||
await page.getByRole("link", { name: "Sign in" }).click();
|
||||
await page.getByPlaceholder("text").click();
|
||||
await page.getByPlaceholder("text").fill(username);
|
||||
await page.getByPlaceholder("text").press("Tab");
|
||||
await page.getByPlaceholder("password").fill(password);
|
||||
await page.getByPlaceholder("password").press("Enter");
|
||||
await page.waitForSelector(`a[href="${LogoutURL}"]`);
|
||||
await page.getByRole("link", { name: "Logout" }).click();
|
||||
await page.waitForSelector(`a[href="${SignInURL}"]`);
|
||||
});
|
||||
});
|
||||
|
||||
it("wrong password", async function () {
|
||||
await withProdApp(async ({ base_url }) => {
|
||||
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||
await page.goto(base_url);
|
||||
await page.getByRole("link", { name: "Sign in" }).click();
|
||||
await page.getByPlaceholder("text").click();
|
||||
await page.getByPlaceholder("text").fill(username);
|
||||
await page.getByPlaceholder("text").press("Tab");
|
||||
await page.getByPlaceholder("password").fill("asddasads20230720722");
|
||||
await page.getByPlaceholder("password").press("Enter");
|
||||
await page.waitForSelector(".form-message");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,139 @@
|
||||
import { Context } from "koa";
|
||||
import {
|
||||
Form,
|
||||
FormData,
|
||||
FormDataValue,
|
||||
Fields,
|
||||
Controls,
|
||||
FormReaction,
|
||||
} from "@sealcode/sealgen";
|
||||
import html from "../html";
|
||||
import { Users } from "../collections/collections";
|
||||
|
||||
export const actionName = "SignUp";
|
||||
|
||||
const fields = {
|
||||
username: new Fields.CollectionField(true, Users.fields.username),
|
||||
email: new Fields.CollectionField(true, Users.fields.email),
|
||||
password: new Fields.SimpleFormField(true),
|
||||
};
|
||||
|
||||
export const SignUpShape = Fields.fieldsToShape(fields);
|
||||
|
||||
export default new (class SignUpForm extends Form<typeof fields, void> {
|
||||
defaultSuccessMessage = "Formularz wypełniony poprawnie";
|
||||
fields = fields;
|
||||
|
||||
controls = [
|
||||
new Controls.SimpleInput(fields.username, { label: "Username:", type: "text" }),
|
||||
new Controls.SimpleInput(fields.email, { label: "Email:", type: "email" }),
|
||||
new Controls.SimpleInput(fields.password, {
|
||||
label: "Password:",
|
||||
type: "password",
|
||||
}),
|
||||
];
|
||||
|
||||
async validateValues(
|
||||
ctx: Context,
|
||||
data: Record<string, FormDataValue>
|
||||
): Promise<{ valid: boolean; error: string }> {
|
||||
const { parsed: email } = await this.fields.email.getValue(ctx, data);
|
||||
const { parsed: password } = await this.fields.password.getValue(ctx, data);
|
||||
|
||||
if ((password || "").length >= 8) {
|
||||
const user = await Users.suList().filter({ email: email }).fetch();
|
||||
if (user.empty) {
|
||||
return { valid: true, error: `` };
|
||||
}
|
||||
return { valid: false, error: `Email is arleady taken` };
|
||||
} else {
|
||||
return {
|
||||
valid: false,
|
||||
error: "Password must contain a minimum of 8 characters",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async canAccess(ctx: Context) {
|
||||
if (ctx.$context.session_id) {
|
||||
return { canAccess: false, message: "" };
|
||||
}
|
||||
return { canAccess: true, message: "" };
|
||||
}
|
||||
|
||||
async onError(
|
||||
ctx: Context,
|
||||
data: FormData<string>,
|
||||
error: unknown
|
||||
): Promise<FormReaction> {
|
||||
const reaction: FormReaction = {
|
||||
action: "stay",
|
||||
content: this.render(ctx, data, true),
|
||||
messages: [
|
||||
{
|
||||
type: "error",
|
||||
text: `An unexpected error occurred, try again. <br> Error${
|
||||
error as string
|
||||
}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return reaction;
|
||||
}
|
||||
|
||||
async onSuccess(ctx: Context, data: FormData): Promise<FormReaction> {
|
||||
const username: FormDataValue = data.raw_values.username;
|
||||
const reaction: FormReaction = {
|
||||
action: "stay",
|
||||
content: `Hello ${String(
|
||||
username
|
||||
)}. <p class="success-notify">Your account has been successfully created.</p>
|
||||
<a href="/" class="nav-logo">
|
||||
<img
|
||||
src="/assets/logo"
|
||||
alt="${ctx.$app.manifest.name} - logo"
|
||||
width="50"
|
||||
height="50"
|
||||
/>
|
||||
Sealious App
|
||||
</a>`,
|
||||
messages: [
|
||||
{
|
||||
type: "success",
|
||||
text: "",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return reaction;
|
||||
}
|
||||
|
||||
async onSubmit(ctx: Context, data: FormData) {
|
||||
const username: string =
|
||||
typeof data.raw_values.username === "string" ? data.raw_values.username : "";
|
||||
const password: string =
|
||||
typeof data.raw_values.password === "string" ? data.raw_values.password : "";
|
||||
const email: string =
|
||||
typeof data.raw_values.email === "string" ? data.raw_values.email : "";
|
||||
|
||||
try {
|
||||
await Users.suCreate({
|
||||
username: username,
|
||||
password: password,
|
||||
email: email,
|
||||
roles: [],
|
||||
});
|
||||
console.log("A user was created successfully.");
|
||||
} catch (error) {
|
||||
console.error("Error during user creation:", error);
|
||||
throw new Error(String(error));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async render(ctx: Context, data: FormData, show_field_errors: boolean) {
|
||||
return html(ctx, "SignUp", await super.render(ctx, data, show_field_errors));
|
||||
}
|
||||
})();
|
@ -0,0 +1,122 @@
|
||||
import { withProdApp } from "../test_utils/with-prod-app";
|
||||
import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint";
|
||||
import { SignUpURL, LogoutURL, SignInURL } from "./urls";
|
||||
import { Browser, BrowserContext, Page } from "@playwright/test";
|
||||
import { getBrowser } from "../test_utils/browser-creator";
|
||||
import ADMIN_CREDENTIALS from "../default-admin-credentials";
|
||||
|
||||
describe("SignUp", () => {
|
||||
let page: Page;
|
||||
let browser: Browser;
|
||||
let context: BrowserContext;
|
||||
const username = ADMIN_CREDENTIALS.username;
|
||||
const password = ADMIN_CREDENTIALS.password;
|
||||
const email = ADMIN_CREDENTIALS.email;
|
||||
|
||||
beforeEach(async () => {
|
||||
browser = await getBrowser();
|
||||
context = await browser.newContext();
|
||||
page = await context.newPage();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await context.close();
|
||||
});
|
||||
|
||||
it("doesn't crash", async function () {
|
||||
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||
return withProdApp(async ({ base_url, rest_api }) => {
|
||||
await rest_api.get(SignUpURL);
|
||||
await webhintURL(base_url + SignUpURL);
|
||||
// alternatively you can use webhintHTML for faster but less precise scans
|
||||
// or for scanning responses of requests that use some form of authorization:
|
||||
// const response = await rest_api.get(SignUpURL);
|
||||
// await webhintHTML(response);
|
||||
});
|
||||
});
|
||||
|
||||
describe("signup test", () => {
|
||||
it("username is taken", async function () {
|
||||
await withProdApp(async ({ base_url }) => {
|
||||
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||
await page.goto(base_url);
|
||||
await page.getByRole("link", { name: "Sign up" }).click();
|
||||
await page.getByPlaceholder("text").click();
|
||||
await page.getByPlaceholder("text").fill(username);
|
||||
await page.getByPlaceholder("text").press("Tab");
|
||||
await page.getByPlaceholder("email").fill("user0192939@randomsuper.com");
|
||||
await page.getByPlaceholder("email").press("Tab");
|
||||
await page.getByPlaceholder("password").fill("user12341234");
|
||||
await page.getByRole("button", { name: "Wyślij" }).click();
|
||||
await page.waitForSelector(".input__error");
|
||||
});
|
||||
});
|
||||
it("password is too shot ", async function () {
|
||||
await withProdApp(async ({ base_url }) => {
|
||||
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||
await page.goto(base_url);
|
||||
await page.getByRole("link", { name: "Sign up" }).click();
|
||||
await page.getByPlaceholder("text").click();
|
||||
await page.getByPlaceholder("text").fill("dasdsa");
|
||||
await page.getByPlaceholder("email").click();
|
||||
await page
|
||||
.getByPlaceholder("email")
|
||||
.fill("asasdsdadsadss123asddsa@asdasca.com");
|
||||
await page.getByPlaceholder("password").click();
|
||||
await page.getByPlaceholder("password").fill("asddsa");
|
||||
await page.getByRole("button", { name: "Wyślij" }).click();
|
||||
await page.waitForSelector(".form-message.form-message--error");
|
||||
});
|
||||
});
|
||||
it("email is taken", async function () {
|
||||
await withProdApp(async ({ base_url }) => {
|
||||
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||
await page.goto(base_url);
|
||||
await page.getByRole("link", { name: "Sign up" }).click();
|
||||
await page.getByPlaceholder("text").click();
|
||||
await page.getByPlaceholder("text").fill("ranomusername2023072722");
|
||||
await page.getByPlaceholder("text").press("Tab");
|
||||
await page.getByPlaceholder("email").fill(email);
|
||||
await page.getByPlaceholder("email").press("Tab");
|
||||
await page.getByPlaceholder("password").fill("asdasdasdasdasd");
|
||||
await page.getByRole("button", { name: "Wyślij" }).click();
|
||||
await page.waitForSelector(".form-message.form-message--error");
|
||||
});
|
||||
});
|
||||
it("correct", async function () {
|
||||
await withProdApp(async ({ base_url }) => {
|
||||
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||
await page.goto(base_url);
|
||||
await page.getByRole("link", { name: "Sign up" }).click();
|
||||
await page.getByPlaceholder("text").click();
|
||||
await page.getByPlaceholder("text").fill("ranomusername20230720722");
|
||||
await page.getByPlaceholder("text").press("Tab");
|
||||
await page.getByPlaceholder("email").fill("radomemail@emailrandom.com");
|
||||
await page.getByPlaceholder("email").press("Tab");
|
||||
await page.getByPlaceholder("password").fill("asdasdasdasdasd");
|
||||
await page.getByRole("button", { name: "Wyślij" }).click();
|
||||
await page.waitForSelector(".success-notify");
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("can access test", () => {
|
||||
it("access url", async function () {
|
||||
await withProdApp(async ({ base_url }) => {
|
||||
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||
await page.goto(base_url);
|
||||
await page.getByRole("link", { name: "Sign in" }).click();
|
||||
await page.getByPlaceholder("text").click();
|
||||
await page.getByPlaceholder("text").fill(username);
|
||||
await page.getByPlaceholder("text").press("Tab");
|
||||
await page.getByPlaceholder("password").fill(password);
|
||||
await page.getByPlaceholder("password").press("Enter");
|
||||
await page.waitForSelector(`a[href="${LogoutURL}"]`);
|
||||
await page.goto(base_url + SignUpURL);
|
||||
await page.waitForSelector('body:has-text("no access")');
|
||||
await page.goto(base_url);
|
||||
await page.getByRole("link", { name: "Logout" }).click();
|
||||
await page.waitForSelector(`a[href="${SignInURL}"]`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,107 @@
|
||||
import { Tasks } from "./../collections/collections";
|
||||
import { tempstream } from "tempstream";
|
||||
import { Context } from "koa";
|
||||
import { Form, FormData, FormDataValue, Fields, Controls } from "@sealcode/sealgen";
|
||||
import html from "../html";
|
||||
import { TaskList } from "./common/tasks-view";
|
||||
|
||||
export const actionName = "Todo";
|
||||
|
||||
const fields = {
|
||||
name: new Fields.CollectionField(true, Tasks.fields.title),
|
||||
};
|
||||
|
||||
export const TodoShape = Fields.fieldsToShape(fields);
|
||||
|
||||
export default new (class TodoForm extends Form<typeof fields, void> {
|
||||
defaultSuccessMessage = "Task has been successfully created";
|
||||
fields = fields;
|
||||
|
||||
controls = [
|
||||
new Controls.SimpleInput(fields.name, {
|
||||
label: "Task name:",
|
||||
type: "text",
|
||||
placeholder: "Write an Matrix bot",
|
||||
}),
|
||||
new Controls.HTML("decoration", (fctx) => {
|
||||
return `<input class="hidden-button" type="hidden" id="action" name="action" value="create" form="${fctx.form_id}" />`;
|
||||
}),
|
||||
];
|
||||
|
||||
async validateValues(
|
||||
ctx: Context,
|
||||
data: Record<string, FormDataValue>
|
||||
): Promise<{ valid: boolean; error: string }> {
|
||||
const { parsed: name } = await this.fields.name.getValue(ctx, data);
|
||||
|
||||
if ((name || "").length < 3) {
|
||||
return {
|
||||
valid: true,
|
||||
error: "The name of the task must have at least 3 characters",
|
||||
};
|
||||
} else {
|
||||
const filter: object = name ? { title: name } : {};
|
||||
|
||||
const tasks = await ctx.$app.collections.tasks
|
||||
.list(ctx.$context)
|
||||
.filter(filter)
|
||||
.fetch();
|
||||
if (tasks.empty) {
|
||||
return { valid: true, error: "" };
|
||||
}
|
||||
return { valid: false, error: "Task with the same name already exists" };
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async canAccess(ctx: Context) {
|
||||
if (ctx.$context.session_id) {
|
||||
return { canAccess: true, message: "" };
|
||||
}
|
||||
return { canAccess: false, message: "" };
|
||||
}
|
||||
|
||||
async onSubmit(ctx: Context, data: FormData) {
|
||||
const action: FormDataValue = data.raw_values.action;
|
||||
|
||||
switch (action) {
|
||||
case "create": {
|
||||
try {
|
||||
await ctx.$app.collections.tasks.create(ctx.$context, {
|
||||
title: String(data.raw_values.name),
|
||||
done: false,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error();
|
||||
}
|
||||
console.debug(`task has been successfully created`);
|
||||
break;
|
||||
}
|
||||
case "delete": {
|
||||
const task = await ctx.$app.collections.tasks.getByID(
|
||||
ctx.$context,
|
||||
data.raw_values.taskId as string
|
||||
);
|
||||
await task.remove(ctx.$context);
|
||||
console.debug(`task has been successfully removed`);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.debug("Wrong action");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
async render(ctx: Context, data: FormData, show_field_errors: boolean) {
|
||||
return html(
|
||||
ctx,
|
||||
"Todo",
|
||||
tempstream`${await super.render(ctx, data, show_field_errors)}
|
||||
${TaskList(ctx)}
|
||||
`
|
||||
);
|
||||
}
|
||||
})();
|
@ -0,0 +1,81 @@
|
||||
import assert from "assert";
|
||||
import { withProdApp } from "../test_utils/with-prod-app";
|
||||
import { LONG_TEST_TIMEOUT, VERY_LONG_TEST_TIMEOUT } from "../test_utils/webhint";
|
||||
import { SignInURL, TodoURL } from "./urls";
|
||||
import { Browser, BrowserContext, Page } from "@playwright/test";
|
||||
import { getBrowser } from "../test_utils/browser-creator";
|
||||
import ADMIN_CREDENTIALS from "../default-admin-credentials";
|
||||
|
||||
describe("Todo", function () {
|
||||
let page: Page;
|
||||
let browser: Browser;
|
||||
let context: BrowserContext;
|
||||
const username = ADMIN_CREDENTIALS.username;
|
||||
const password = ADMIN_CREDENTIALS.password;
|
||||
|
||||
beforeEach(async () => {
|
||||
browser = await getBrowser();
|
||||
context = await browser.newContext();
|
||||
page = await context.newPage();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await context.close();
|
||||
});
|
||||
|
||||
it("doesn't crash", async function () {
|
||||
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||
return withProdApp(async ({ rest_api }) => {
|
||||
await assert.rejects(
|
||||
async () => {
|
||||
await rest_api.get(TodoURL);
|
||||
},
|
||||
{ name: "Error" }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("todo test", () => {
|
||||
it("create and delete task", async function () {
|
||||
await withProdApp(async ({ base_url }) => {
|
||||
this.timeout(VERY_LONG_TEST_TIMEOUT);
|
||||
await page.goto(base_url);
|
||||
await page.getByRole("link", { name: "Sign in" }).click();
|
||||
await page.getByPlaceholder("text").click();
|
||||
await page.getByPlaceholder("text").fill(username);
|
||||
await page.getByPlaceholder("text").press("Tab");
|
||||
await page.getByPlaceholder("password").fill(password);
|
||||
await page.getByPlaceholder("password").press("Enter");
|
||||
await page.getByRole("link", { name: "To do app" }).click();
|
||||
await page.getByPlaceholder("Write an Matrix bot").click();
|
||||
await page.getByPlaceholder("Write an Matrix bot").fill("randomtasdk");
|
||||
await page.getByRole("button", { name: "Wyślij" }).click();
|
||||
await page.waitForSelector(".form-message.form-message--success");
|
||||
await page.locator("turbo-frame").getByRole("checkbox").check();
|
||||
await page.locator("turbo-frame").getByRole("checkbox").uncheck();
|
||||
await page
|
||||
.locator("turbo-frame")
|
||||
.getByRole("button", { name: "Delete" })
|
||||
.click();
|
||||
await page.getByRole("link", { name: "Logout" }).click();
|
||||
await page.waitForSelector(`a[href="${SignInURL}"]`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("can access test", () => {
|
||||
it("access url", async function () {
|
||||
await withProdApp(async ({ base_url }) => {
|
||||
this.timeout(LONG_TEST_TIMEOUT);
|
||||
await page.goto(base_url);
|
||||
try {
|
||||
await page.waitForSelector(`a[href="${SignInURL}"]`);
|
||||
await page.goto(base_url + TodoURL);
|
||||
await page.waitForSelector('body:has-text("no access")');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1 +1,5 @@
|
||||
export const HelloURL = "/hello/";
|
||||
export const LogoutURL = "/logout/";
|
||||
export const SignInURL = "/signIn/";
|
||||
export const SignUpURL = "/signUp/";
|
||||
export const TodoURL = "/todo/";
|
||||
|
@ -1,5 +1,5 @@
|
||||
describe("sample test", () => {
|
||||
it("always passes", () => {
|
||||
return true;
|
||||
return;
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,16 @@
|
||||
import { Browser, firefox } from "@playwright/test";
|
||||
|
||||
let browser: Browser;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export async function getBrowser(): Promise<Browser> {
|
||||
if (!browser) browser = await firefox.launch();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return browser;
|
||||
}
|
||||
|
||||
export async function closeBrowser() {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import { Controller } from "stimulus";
|
||||
|
||||
export default class TaskController extends Controller {
|
||||
id: string;
|
||||
|
||||
connect() {
|
||||
const dataIdAttr = this.element.getAttribute("data-id");
|
||||
if (dataIdAttr) {
|
||||
this.id = dataIdAttr;
|
||||
}
|
||||
}
|
||||
|
||||
async toggle(event: Event) {
|
||||
const inputElement: HTMLInputElement = event.target as HTMLInputElement;
|
||||
|
||||
if (inputElement instanceof HTMLInputElement) {
|
||||
const isChecked: boolean = inputElement.checked;
|
||||
|
||||
await fetch(`/api/v1/collections/tasks/${this.id}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
done: isChecked,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import * as Turbo from "@hotwired/turbo";
|
||||
import { Application } from "stimulus";
|
||||
import TaskController from "./controllers/task-controller";
|
||||
|
||||
export { Turbo };
|
||||
|
||||
const application = Application.start();
|
||||
application.register("task", TaskController);
|
||||
|
Loading…
Reference in New Issue