Initialize playwright, fix file upload issue, show publication status and delete website dir on website deletion

This commit is contained in:
thiloho
2024-08-30 15:48:15 +02:00
parent 8915a7cfd9
commit bb73c2350d
15 changed files with 284 additions and 41 deletions

View File

@@ -29,7 +29,13 @@
alias formatsql="${pkgs.pgformatter}/bin/pg_format -s 2 -f 2 -U 2 -i db/migrations/*.sql"
'';
};
web = pkgs.mkShell { packages = with pkgs; [ nodejs_22 ]; };
web = pkgs.mkShell {
packages = with pkgs; [ nodejs_22 ];
shellHook = ''
export PLAYWRIGHT_BROWSERS_PATH=${pkgs.playwright-driver.browsers}
export PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=true
'';
};
}
);

View File

@@ -13,9 +13,7 @@ let
web = buildNpmPackage {
name = "web-app";
src = ../web-app;
npmDeps = importNpmLock {
npmRoot = ../web-app;
};
npmDeps = importNpmLock { npmRoot = ../web-app; };
npmConfigHook = importNpmLock.npmConfigHook;
npmFlags = [ "--legacy-peer-deps" ];
installPhase = ''

View File

@@ -0,0 +1,8 @@
-- migrate:up
ALTER TABLE internal.article
ALTER COLUMN publication_date DROP NOT NULL;
-- migrate:down
ALTER TABLE internal.article
ALTER COLUMN publication_date SET NOT NULL;

View File

@@ -0,0 +1,35 @@
-- migrate:up
CREATE OR REPLACE VIEW api.website WITH ( security_invoker = ON
) AS
SELECT
id,
user_id,
content_type,
title,
created_at,
last_modified_at,
last_modified_by,
title_search,
is_published -- New column
FROM
internal.website;
GRANT SELECT, UPDATE, DELETE ON api.website TO authenticated_user;
-- migrate:down
DROP VIEW api.website;
CREATE OR REPLACE VIEW api.website WITH ( security_invoker = ON
) AS
SELECT
id,
user_id,
content_type,
title,
created_at,
last_modified_at,
last_modified_by,
title_search
FROM
internal.website;

View File

@@ -15,6 +15,7 @@
"marked-highlight": "2.1.4"
},
"devDependencies": {
"@playwright/test": "1.40.0",
"@sveltejs/adapter-auto": "3.2.4",
"@sveltejs/adapter-node": "5.2.2",
"@sveltejs/kit": "2.5.22",
@@ -515,6 +516,22 @@
"node": ">=14"
}
},
"node_modules/@playwright/test": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.0.tgz",
"integrity": "sha512-PdW+kn4eV99iP5gxWNSDQCbhMaDVej+RXL5xr6t04nbKLCBwYtA046t7ofoczHOm8u6c+45hpDKQVZqtqwkeQg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.40.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@polka/url": {
"version": "1.0.0-next.25",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz",
@@ -2193,6 +2210,53 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/playwright": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.0.tgz",
"integrity": "sha512-gyHAgQjiDf1m34Xpwzaqb76KgfzYrhK7iih+2IzcOCoZWr/8ZqmdBw+t0RU85ZmfJMgtgAiNtBQ/KS2325INXw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.40.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.0.tgz",
"integrity": "sha512-fvKewVJpGeca8t0ipM56jkVSU6Eo0RmFvQ/MaCQNDYm+sdvKkMBBWTE1FdeMqIdumRaXXjZChWHvIzCGM/tA/Q==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/postcss": {
"version": "8.4.40",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz",

View File

@@ -6,12 +6,14 @@
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"test": "playwright test",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check .",
"format": "prettier --write ."
},
"devDependencies": {
"@playwright/test": "1.40.0",
"@sveltejs/adapter-auto": "3.2.4",
"@sveltejs/adapter-node": "5.2.2",
"@sveltejs/kit": "2.5.22",

View File

@@ -0,0 +1,12 @@
import { type PlaywrightTestConfig } from "@playwright/test";
const config: PlaywrightTestConfig = {
webServer: {
command: "npm run build && npm run preview",
port: 4173
},
testDir: "tests",
testMatch: /(.+\.)?(test|spec)\.ts/
};
export default config;

View File

@@ -1,3 +1,5 @@
import { dev } from "$app/environment";
export const API_BASE_PREFIX = dev ? "http://localhost:3000" : `${process.env.ORIGIN}/api`;
export const API_BASE_PREFIX = dev
? "http://localhost:3000"
: `${process.env.ORIGIN ? `${process.env.ORIGIN}/api` : "http://localhost:3000"}`;

View File

@@ -1,5 +1,7 @@
import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils";
import { rm } from "node:fs/promises";
import { join } from "node:path";
export const load: PageServerLoad = async ({ fetch, cookies, url, locals }) => {
const searchQuery = url.searchParams.get("website_search_query");
@@ -130,6 +132,11 @@ export const actions: Actions = {
return { success: false, message: response.message };
}
await rm(join("/", "var", "www", "archtika-websites", data.get("id") as string), {
recursive: true,
force: true
});
return { success: true, message: "Successfully deleted website" };
}
};

View File

@@ -49,22 +49,27 @@ export const actions: Actions = {
const data = await request.formData();
const faviconFile = data.get("favicon") as File;
const headers: Record<string, string> = {
"Content-Type": "application/octet-stream",
Authorization: `Bearer ${cookies.get("session_token")}`,
Accept: "application/vnd.pgrst.object+json",
"X-Website-Id": params.websiteId
};
if (faviconFile) {
headers["X-Mimetype"] = faviconFile.type;
headers["X-Original-Filename"] = faviconFile.name;
}
const uploadedImageData = await fetch(`${API_BASE_PREFIX}/rpc/upload_file`, {
method: "POST",
headers: {
"Content-Type": "application/octet-stream",
Authorization: `Bearer ${cookies.get("session_token")}`,
Accept: "application/vnd.pgrst.object+json",
"X-Website-Id": params.websiteId,
"X-Mimetype": faviconFile.type,
"X-Original-Filename": faviconFile.name
},
body: await faviconFile.arrayBuffer()
headers,
body: faviconFile ? await faviconFile.arrayBuffer() : null
});
const uploadedImage = await uploadedImageData.json();
if (!uploadedImageData.ok && faviconFile.size > 0) {
if (!uploadedImageData.ok && (faviconFile?.size ?? 0 > 0)) {
return { success: false, message: uploadedImage.message };
}
@@ -95,22 +100,27 @@ export const actions: Actions = {
const data = await request.formData();
const logoImage = data.get("logo-image") as File;
const headers: Record<string, string> = {
"Content-Type": "application/octet-stream",
Authorization: `Bearer ${cookies.get("session_token")}`,
Accept: "application/vnd.pgrst.object+json",
"X-Website-Id": params.websiteId
};
if (logoImage) {
headers["X-Mimetype"] = logoImage.type;
headers["X-Original-Filename"] = logoImage.name;
}
const uploadedImageData = await fetch(`${API_BASE_PREFIX}/rpc/upload_file`, {
method: "POST",
headers: {
"Content-Type": "application/octet-stream",
Authorization: `Bearer ${cookies.get("session_token")}`,
Accept: "application/vnd.pgrst.object+json",
"X-Website-Id": params.websiteId,
"X-Mimetype": logoImage.type,
"X-Original-Filename": logoImage.name
},
body: await logoImage.arrayBuffer()
headers,
body: logoImage ? await logoImage.arrayBuffer() : null
});
const uploadedImage = await uploadedImageData.json();
if (!uploadedImageData.ok && logoImage.size > 0) {
if (!uploadedImageData.ok && (logoImage?.size ?? 0 > 0)) {
return { success: false, message: uploadedImage.message };
}

View File

@@ -123,12 +123,7 @@
<div class="file-field">
<label>
Logo image:
<input
type="file"
name="logo-image"
accept={ALLOWED_MIME_TYPES.join(", ")}
required={data.header.logo_type === "image"}
/>
<input type="file" name="logo-image" accept={ALLOWED_MIME_TYPES.join(", ")} />
</label>
{#if data.header.logo_image}
<Modal id="preview-logo-header-{data.header.website_id}" text="Preview">

View File

@@ -34,22 +34,27 @@ export const actions: Actions = {
const data = await request.formData();
const coverFile = data.get("cover-image") as File;
const headers: Record<string, string> = {
"Content-Type": "application/octet-stream",
Authorization: `Bearer ${cookies.get("session_token")}`,
Accept: "application/vnd.pgrst.object+json",
"X-Website-Id": params.websiteId
};
if (coverFile) {
headers["X-Mimetype"] = coverFile.type;
headers["X-Original-Filename"] = coverFile.name;
}
const uploadedImageData = await fetch(`${API_BASE_PREFIX}/rpc/upload_file`, {
method: "POST",
headers: {
"Content-Type": "application/octet-stream",
Authorization: `Bearer ${cookies.get("session_token")}`,
Accept: "application/vnd.pgrst.object+json",
"X-Website-Id": params.websiteId,
"X-Mimetype": coverFile.type,
"X-Original-Filename": coverFile.name
},
body: await coverFile.arrayBuffer()
headers,
body: coverFile ? await coverFile.arrayBuffer() : null
});
const uploadedImage = await uploadedImageData.json();
if (!uploadedImageData.ok && coverFile.size > 0) {
if (!uploadedImageData.ok && (coverFile?.size ?? 0 > 0)) {
return { success: false, message: uploadedImage.message };
}

View File

@@ -54,6 +54,22 @@ export const actions: Actions = {
const websiteOverview = await websiteOverviewData.json();
generateStaticFiles(websiteOverview, false);
const res = await fetch(`${API_BASE_PREFIX}/website?id=eq.${params.websiteId}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${cookies.get("session_token")}`
},
body: JSON.stringify({
is_published: true
})
});
if (!res.ok) {
const response = await res.json();
return { success: false, message: response.message };
}
return { success: true, message: "Successfully published website" };
}
};

View File

@@ -5,6 +5,8 @@
import type { ActionData, PageServerData } from "./$types";
const { data, form }: { data: PageServerData; form: ActionData } = $props();
const prodWebsiteUrl = data.websitePreviewUrl.replace("/previews", "");
</script>
<SuccessOrError success={form?.success} message={form?.message} />
@@ -28,5 +30,18 @@
<form method="POST" action="?/publishWebsite" use:enhance>
<button type="submit">Publish</button>
</form>
{#if data.website.is_published}
<section>
<h3>
<a href="#publication-status">Publication status</a>
</h3>
<p>
Your website is published at:
<br />
<a href={prodWebsiteUrl}>{prodWebsiteUrl}</a>
</p>
</section>
{/if}
</section>
</WebsiteEditor>

View File

@@ -0,0 +1,68 @@
import { test, expect } from "@playwright/test";
test("Register", async ({ page }) => {
await page.goto("/");
await page.getByRole("link", { name: "Register" }).click();
await page.getByLabel("Username:").click();
await page.getByLabel("Username:").fill("archtika-test");
await page.getByLabel("Password:").click();
await page.getByLabel("Password:").fill("T3stuser??!!");
await page.getByRole("button", { name: "Submit" }).click();
await expect(page.getByText("Successfully registered, you")).toBeVisible();
});
test("Login", async ({ page }) => {
await page.goto("/");
await page.getByRole("link", { name: "Login" }).click();
await page.getByLabel("Username:").click();
await page.getByLabel("Username:").fill("archtika-test");
await page.getByLabel("Password:").click();
await page.getByLabel("Password:").fill("T3stuser??!!");
await page.getByRole("button", { name: "Submit" }).click();
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
});
test("Logout", async ({ page }) => {
await page.goto("/");
await page.getByRole("link", { name: "Login" }).click();
await page.getByLabel("Username:").click();
await page.getByLabel("Username:").fill("archtika-test");
await page.getByLabel("Password:").click();
await page.getByLabel("Password:").fill("T3stuser??!!");
await page.getByRole("button", { name: "Submit" }).click();
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
await page.getByRole("link", { name: "Account" }).click();
await page.getByRole("button", { name: "Logout" }).click();
await expect(page.getByRole("heading", { name: "Login" })).toBeVisible();
});
test("Delete account", async ({ page }) => {
await page.goto("/");
await page.getByRole("link", { name: "Login" }).click();
await page.getByLabel("Username:").click();
await page.getByLabel("Username:").fill("archtika-test");
await page.getByLabel("Password:").click();
await page.getByLabel("Password:").fill("T3stuser??!!");
await page.getByRole("button", { name: "Submit" }).click();
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
await page.getByRole("link", { name: "Account" }).click();
await page.getByRole("button", { name: "Delete account" }).click();
await page.getByLabel("Password:").click();
await page.getByLabel("Password:").fill("T3stuser??!!");
await page
.locator("#delete-account-modal")
.getByRole("button", { name: "Delete account" })
.click();
await expect(page.getByRole("heading", { name: "Login" })).toBeVisible();
});
test("Register after account deletion", async ({ page }) => {
await page.goto("/");
await page.getByRole("link", { name: "Register" }).click();
await page.getByLabel("Username:").click();
await page.getByLabel("Username:").fill("archtika-test");
await page.getByLabel("Password:").click();
await page.getByLabel("Password:").fill("T3stuser??!!");
await page.getByRole("button", { name: "Submit" }).click();
await expect(page.getByText("Successfully registered, you")).toBeVisible();
});