Coverage test

Summary: --

Reviewers: #reviewers

Subscribers: jenkins-user

Differential Revision: https://hub.sealcode.org/D1606
This commit is contained in:
Kuba Orlik 2025-07-22 17:20:47 +02:00
parent 6b470fa709
commit 10ef8cd3d2
38 changed files with 16304 additions and 4378 deletions

2
.gitignore vendored
View File

@ -49,3 +49,5 @@ lint-report.json
/meili_data/ /meili_data/
!/meili_data/.keepme !/meili_data/.keepme
/src/colors.html /src/colors.html
coverage.json
/test-results/

View File

@ -0,0 +1,42 @@
import fs from "fs";
import xml2js from "xml2js";
function convertCoberturaToArcanistJSON(coberturaXml) {
const parser = new xml2js.Parser();
return parser.parseStringPromise(coberturaXml).then((result) => {
const coverage = {};
const classes = result.coverage.packages[0].package.flatMap(
(pkg) => pkg.classes[0].class
);
for (const cls of classes) {
const filePath = cls.$.filename;
const lines = cls.lines[0].line;
const lineCoverage = [];
let maxLine = 0;
for (const line of lines) {
const num = parseInt(line.$.number, 10);
const hits = parseInt(line.$.hits, 10);
if (num > maxLine) maxLine = num;
lineCoverage[num] = hits > 0 ? "C" : "U";
}
// Fill gaps with 'N'
for (let i = 1; i <= maxLine; i++) {
if (!lineCoverage[i]) lineCoverage[i] = "N";
}
const coverageStr = lineCoverage.slice(1).join("");
coverage[filePath] = coverageStr;
}
return coverage;
});
}
const coberturaXml = fs.readFileSync("coverage/cobertura-coverage.xml", "utf8");
convertCoberturaToArcanistJSON(coberturaXml).then((json) => {
console.log(JSON.stringify(json));
});

View File

@ -4,6 +4,7 @@ services:
image: mongo:4.4-bionic image: mongo:4.4-bionic
ports: ports:
- "127.0.0.1:${PORT:-2076}7:27017" - "127.0.0.1:${PORT:-2076}7:27017"
test: test:
image: sealious-app:latest image: sealious-app:latest
build: build:
@ -12,14 +13,16 @@ services:
volumes: volumes:
- ./:/opt/sealious-app/ - ./:/opt/sealious-app/
- /tmp:/tmp - /tmp:/tmp
- ./docker_node_modules:/opt/sealious-app/node_modules
user: ${USER_ID:-1000}:${GROUP_ID:-1000} user: ${USER_ID:-1000}:${GROUP_ID:-1000}
command: tail -f /dev/null # to keep it open
mailcatcher: mailcatcher:
image: schickling/mailcatcher:latest image: schickling/mailcatcher:latest
ports: ports:
- "127.0.0.1:${PORT:-108}2:1080" - "127.0.0.1:${PORT:-108}2:1080"
- "127.0.0.1:${PORT:-102}6:1025" - "127.0.0.1:${PORT:-102}6:1025"
user: ${USER_ID:-1000}:${GROUP_ID:-1000} user: ${USER_ID:-1000}:${GROUP_ID:-1000}
meilisearch: meilisearch:
image: getmeili/meilisearch:v1.9 image: getmeili/meilisearch:v1.9
ports: ports:
@ -30,3 +33,11 @@ services:
MEILI_ENV: development MEILI_ENV: development
MEILI_MASTER_KEY: ${MEILISEARCH_MASTER_KEY:-qwerty} MEILI_MASTER_KEY: ${MEILISEARCH_MASTER_KEY:-qwerty}
user: ${USER_ID:-1000}:${GROUP_ID:-1000} user: ${USER_ID:-1000}:${GROUP_ID:-1000}
playwright:
image: mcr.microsoft.com/playwright:v1.54.0-noble
ports:
- 127.0.0.1:3000:3000
extra_hosts:
- "host.docker.internal:host-gateway"
command: npx -y playwright@1.54.0 run-server --port 3000 --host 0.0.0.0

View File

@ -1,4 +1,4 @@
FROM node:21-bullseye-slim FROM node:22-bullseye-slim
ENV HOME=/opt/sealious-app ENV HOME=/opt/sealious-app
@ -10,11 +10,6 @@ RUN apt update
RUN apt install -y git RUN apt install -y git
RUN apt install -y tmux RUN apt install -y tmux
# playwright deps
RUN apt-get update&& apt-get install -y --no-install-recommends libasound2 libatk-bridge2.0-0 libatk1.0-0 libatspi2.0-0 libcairo2 libcups2 libdbus-1-3 libdrm2 libgbm1 libglib2.0-0 libnspr4 libnss3 libpango-1.0-0 libwayland-client0 libx11-6 libxcb1 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 xvfb fonts-noto-color-emoji fonts-unifont libfontconfig1 libfreetype6 xfonts-cyrillic xfonts-scalable fonts-liberation fonts-ipafont-gothic fonts-wqy-zenhei fonts-tlwg-loma-otf fonts-freefont-ttf libcairo-gobject2 libdbus-glib-1-2 libgdk-pixbuf-2.0-0 libgtk-3-0 libharfbuzz0b libpangocairo-1.0-0 libx11-xcb1 libxcb-shm0 libxcursor1 libxi6 libxrender1 libxtst6 gstreamer1.0-libav gstreamer1.0-plugins-bad gstreamer1.0-plugins-base gstreamer1.0-plugins-good libegl1 libenchant-2-2 libepoxy0 libevdev2 libgles2 libglx0 libgstreamer-gl1.0-0 libgstreamer-plugins-base1.0-0 libgstreamer1.0-0 libgudev-1.0-0 libharfbuzz-icu0 libhyphen0 libicu67 libjpeg62-turbo liblcms2-2 libmanette-0.2-0 libnotify4 libopengl0 libopenjp2-7 libopus0 libpng16-16 libproxy1v5 libsecret-1-0 libsoup2.4-1 libwayland-egl1 libwayland-server0 libwebp6 libwebpdemux2 libwoff1 libxml2 libxslt1.1 libatomic1 libevent-2.1-7 python
RUN apt install wget
RUN chmod +x /tini RUN chmod +x /tini
ENTRYPOINT ["/tini", "--"] ENTRYPOINT ["/tini", "--"]

View File

@ -5,7 +5,6 @@ SEALIOUS_BASE_URL=$(cat .base_url)
export SEALIOUS_BASE_URL export SEALIOUS_BASE_URL
./npm.sh ci --no-progress --no-color ./npm.sh ci --no-progress --no-color
./npm.sh run install-test-deps --no-progress --no-color
./npm.sh run build --no-progress --no-color --ignore-scripts ./npm.sh run build --no-progress --no-color --ignore-scripts
./npm.sh run typecheck --no-progress --no-color --ignore-scripts ./npm.sh run typecheck --no-progress --no-color --ignore-scripts
@ -59,10 +58,16 @@ EOM
node -e "$parse_command" > lint-report.json node -e "$parse_command" > lint-report.json
docker-compose up -d meilisearch mailcatcher docker-compose up -d meilisearch mailcatcher playwright
CONTAINER_ID=$(docker compose run -d --user="$UID" \ docker compose run --user="$UID" \
-e "FORCE_COLOR=0" \
test npx playwright install firefox
docker compose up -d test
docker compose exec -T --user="$UID" \
-e "SEALIOUS_MONGO_PORT=27017" \ -e "SEALIOUS_MONGO_PORT=27017" \
-e "SEALIOUS_MONGO_HOST=db" \ -e "SEALIOUS_MONGO_HOST=db" \
-e "SEALIOUS_PORT=8080" \ -e "SEALIOUS_PORT=8080" \
@ -72,20 +77,7 @@ CONTAINER_ID=$(docker compose run -d --user="$UID" \
-e "SEALIOUS_MAILCATCHER_API_PORT=1080" \ -e "SEALIOUS_MAILCATCHER_API_PORT=1080" \
-e "SEALIOUS_MAILCATCHER_SMTP_PORT=1025" \ -e "SEALIOUS_MAILCATCHER_SMTP_PORT=1025" \
-e "FORCE_COLOR=0" \ -e "FORCE_COLOR=0" \
test npx playwright install firefox) test \
npm run --ignore-scripts test-ci
CONTAINER_ID=$(docker compose run -d --user="$UID" \ ./npm.sh run --silent cover-arcanist > coverage.json
-e "SEALIOUS_MONGO_PORT=27017" \
-e "SEALIOUS_MONGO_HOST=db" \
-e "SEALIOUS_PORT=8080" \
-e "SEALIOUS_BASE_URL=$SEALIOUS_BASE_URL" \
-e "MEILISEARCH_HOST=http://meilisearch:7700" \
-e "SEALIOUS_MAILCATCHER_HOST=mailcatcher" \
-e "SEALIOUS_MAILCATCHER_API_PORT=1080" \
-e "SEALIOUS_MAILCATCHER_SMTP_PORT=1025" \
-e "FORCE_COLOR=0" \
test npm run test-reports)
docker logs -f $CONTAINER_ID
exit $(docker inspect $CONTAINER_ID --format='{{.State.ExitCode}}')

8
npm.sh
View File

@ -4,13 +4,9 @@
./docker-up.sh ./docker-up.sh
CONTAINER_ID=$(docker-compose run \ docker-compose exec -T \
-d \
--service-ports \
-e "SEALIOUS_MONGO_PORT=27017" \ -e "SEALIOUS_MONGO_PORT=27017" \
-e "SEALIOUS_MONGO_HOST=db" \ -e "SEALIOUS_MONGO_HOST=db" \
test \ test \
npm "$@") npm "$@"
docker logs -f $CONTAINER_ID
docker rm $CONTAINER_ID

19803
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,8 +15,9 @@
"postbuild": "npm run typecheck && npm run lint", "postbuild": "npm run typecheck && npm run lint",
"watch": "multiple-scripts-tmux -p watch", "watch": "multiple-scripts-tmux -p watch",
"reset-db": "docker compose down -v && docker compose up -d", "reset-db": "docker compose down -v && docker compose up -d",
"pretest": "npm run build && docker compose up -d", "pretest": "rm -rf coverage && npm run build && docker compose up -d",
"test": "node test.cjs", "test": "PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:3000/ npx playwright test",
"test-ci": "TESTS_RUN_IN_DOCKER=true PW_TEST_CONNECT_WS_ENDPOINT=ws://playwright:3000/ npx playwright test",
"prepare": "husky", "prepare": "husky",
"lint-css": "stylelint \"**/*.css\" --config ./.stylelintrc.json", "lint-css": "stylelint \"**/*.css\" --config ./.stylelintrc.json",
"lint-css-fix": "npm run lint-css -- --fix", "lint-css-fix": "npm run lint-css -- --fix",
@ -24,11 +25,9 @@
"lint-js": "eslint src", "lint-js": "eslint src",
"lint-js-report": "npm run --silent lint-js -- --format json > .eslint-result.json", "lint-js-report": "npm run --silent lint-js -- --format json > .eslint-result.json",
"lint": "npm run lint-js && npm run lint-css", "lint": "npm run lint-js && npm run lint-css",
"lint-report": "npm run lint-js-report && npm run lint-css-report", "lint-report": "npm run lint-js-report; npm run lint-css-report",
"pretest-reports": "npm run build --ignore-scripts && rm -fr .xunit coverage", "cover-html": "npm run test; c8 report --reporter lcov --all --src src && xdg-open coverage/lcov-report/index.html",
"test-reports": "npx c8 --exclude \"\" ./node_modules/.bin/mocha --recursive --timeout=10000 --require source-map-support/register 'dist/**/*.test.js' --exit && c8 report --reporter cobertura --exclude \"\"", "cover-arcanist": "c8 report --reporter cobertura --all --src src && node ./convert-cobertura-to-arcanist.mjs"
"precover-html": "rm -rf coverage/lcov-report",
"cover-html": "npm run test-reports && c8 report --reporter lcov --exclude \"\" && xdg-open coverage/lcov-report/index.html"
}, },
"tmux-scripts": { "tmux-scripts": {
"watch": [ "watch": [
@ -83,7 +82,7 @@
"@sealcode/file-manager": "^1.0.2", "@sealcode/file-manager": "^1.0.2",
"@sealcode/jdd": "^0.8.0", "@sealcode/jdd": "^0.8.0",
"@sealcode/jdd-editor": "^0.2.0", "@sealcode/jdd-editor": "^0.2.0",
"@sealcode/sealgen": "^0.18.0", "@sealcode/sealgen": "^0.18.5",
"@sealcode/simplemde": "^1.12.1", "@sealcode/simplemde": "^1.12.1",
"@sealcode/ts-predicates": "^0.6.2", "@sealcode/ts-predicates": "^0.6.2",
"@types/kill-port": "^2.0.0", "@types/kill-port": "^2.0.0",
@ -95,6 +94,7 @@
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"escape-goat": "^4.0.0", "escape-goat": "^4.0.0",
"get-port": "^7.0.0", "get-port": "^7.0.0",
"glob": "^11.0.3",
"js-convert-case": "^4.2.0", "js-convert-case": "^4.2.0",
"koa-mount": "^4.2.0", "koa-mount": "^4.2.0",
"koa-qs": "^3.0.0", "koa-qs": "^3.0.0",
@ -102,11 +102,13 @@
"koa-static": "^5.0.0", "koa-static": "^5.0.0",
"locreq": "^3.0.0", "locreq": "^3.0.0",
"meilisearch": "^0.41.0", "meilisearch": "^0.41.0",
"mongodb": "^6.17.0",
"multiple-scripts-tmux": "^1.0.4", "multiple-scripts-tmux": "^1.0.4",
"nodemon": "^3.0.1", "nodemon": "^3.0.1",
"nyc": "^17.1.0",
"object-path": "^0.11.8", "object-path": "^0.11.8",
"qs": "^6.12.0", "qs": "^6.12.0",
"sealious": "^0.21.4", "sealious": "^0.21.7",
"slug": "^9.1.0", "slug": "^9.1.0",
"stimulus": "^3.2.2", "stimulus": "^3.2.2",
"tempstream": "^0.4.6", "tempstream": "^0.4.6",
@ -140,12 +142,14 @@
"kill-port": "^1.6.1", "kill-port": "^1.6.1",
"mocha": "^10.4.0", "mocha": "^10.4.0",
"mri": "^1.2.0", "mri": "^1.2.0",
"playwright": "^1.54.1",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"stylelint": "^16.10.0", "stylelint": "^16.10.0",
"stylelint-config-standard": "^36.0.1", "stylelint-config-standard": "^36.0.1",
"ts-loader": "^8.0.14", "ts-loader": "^8.0.14",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.6" "typescript": "^5.6",
"xml2js": "^0.6.2"
}, },
"engines": { "engines": {
"node": ">=21.0.0" "node": ">=21.0.0"

22
playwright.config.ts Normal file
View File

@ -0,0 +1,22 @@
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "tests",
globalTeardown: "./tests/global-teardown",
projects: [
{
name: "firefox",
use: {
browserName: "firefox",
},
},
],
expect: {
toHaveScreenshot: {
pathTemplate: "{testDir}/__screenshots/{testFilePath}/{arg}{ext}",
},
toMatchAriaSnapshot: {
pathTemplate: "{testDir}/__snapshots/{testFilePath}/{arg}{ext}",
},
},
});

View File

@ -3,7 +3,7 @@ import type { Context } from "koa";
import type { FlatTemplatable } from "tempstream"; import type { FlatTemplatable } from "tempstream";
import type { Context as SealiousContext } from "sealious"; import type { Context as SealiousContext } from "sealious";
import { App, LoggerMailer, SMTPMailer } from "sealious"; import { App, LoggerMailer, SMTPMailer } from "sealious";
import type { LoggerLevel } from "sealious/@types/src/app/logger.js"; import type { LoggerLevel } from "sealious/@types/app/logger.js";
import { collections } from "./collections/collections.js"; import { collections } from "./collections/collections.js";
import { import {
BASE_URL, BASE_URL,

View File

@ -4,7 +4,7 @@ import type { App, CollectionItem } from "sealious";
import { CRUDRoles } from "../policy-types/roles.js"; import { CRUDRoles } from "../policy-types/roles.js";
import assert from "assert"; import assert from "assert";
import TheApp from "../app.js"; import TheApp from "../app.js";
import type { FieldEntryMapping } from "sealious/@types/src/chip-types/collection.js"; import type { FieldEntryMapping } from "sealious/@types/chip-types/collection.js";
import { JDD } from "@sealcode/jdd"; import { JDD } from "@sealcode/jdd";
import { registry } from "../jdd-components/registry.js"; import { registry } from "../jdd-components/registry.js";
import type { RawJDDocument } from "@sealcode/jdd"; import type { RawJDDocument } from "@sealcode/jdd";

View File

@ -1,91 +0,0 @@
import axios from "axios";
import assert from "assert";
import type TheApp from "../app.js";
import { withProdApp } from "../test_utils/with-prod-app.js";
describe("password-reset-intents", function () {
//ts-ignore
async function createAUser(app: TheApp) {
await app.collections.users.suCreate({
username: "user",
email: "user@example.com",
password: "password",
roles: [],
});
}
it("tells you if the email address doesn't exist", async function () {
return withProdApp(async ({ app, base_url }) => {
const email = "fake@example.com";
try {
await axios.post(
`${base_url}/api/v1/collections/password-reset-intents`,
{
email: email,
}
);
} catch (e) {
assert.equal(
e.response.data.data.field_messages.email.message,
app.i18n("invalid_existing_value", ["users", "email", email])
);
return;
}
throw new Error("it didn't throw");
});
});
it("allows anyone to create an intent, if the email exists", async () =>
withProdApp(async ({ app, base_url }) => {
await createAUser(app);
const { email, token } = (
await axios.post(
`${base_url}/api/v1/collections/password-reset-intents`,
{
email: "user@example.com",
}
)
).data;
assert.deepEqual(
{ email, token },
{
email: "user@example.com",
token: "it's a secret to everybody",
}
);
}));
it("tells you if the email address is malformed", async () =>
withProdApp(async ({ app, base_url }) => {
const email = "incorrect-address";
try {
await axios.post(
`${base_url}/api/v1/collections/password-reset-intents`,
{
email: email,
}
);
} catch (e) {
assert.equal(
e.response.data.data.field_messages.email.message,
app.i18n("invalid_email", [email])
);
return;
}
throw new Error("it didn't throw");
}));
it("sends an email with the reset password link", async () =>
withProdApp(async ({ app, base_url, mail_api }) => {
await createAUser(app);
await axios.post(`${base_url}/api/v1/collections/password-reset-intents`, {
email: "user@example.com",
});
const messages = (await mail_api.getMessages()).filter(
(message) => message.recipients[0] == "<user@example.com>"
);
assert.equal(messages.length, 1);
assert.equal(messages[0]?.recipients.length, 1);
assert.equal(messages[0]?.recipients[0], "<user@example.com>");
}));
});

View File

@ -6,6 +6,12 @@ import dotenv from "dotenv";
dotenv.config(); dotenv.config();
export const SEALIOUS_SANITY = Boolean(process.env.SEALIOUS_SANITY); export const SEALIOUS_SANITY = Boolean(process.env.SEALIOUS_SANITY);
// for tests fixtures to be able to close nicely when the test is done - otherwise
// coverage is not reported if we kill the process
export const SEALIOUS_KILLSWITCH_PORT =
parseInt(process.env.SEALIOUS_KILLSWITCH_PORT || "") || null;
export const PORT = process.env.SEALIOUS_PORT export const PORT = process.env.SEALIOUS_PORT
? parseInt(process.env.SEALIOUS_PORT) ? parseInt(process.env.SEALIOUS_PORT)
: 8080; : 8080;

View File

@ -45,18 +45,6 @@ export function defaultHead({
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="${htmlEscape(description)}" /> <meta name="description" content="${htmlEscape(description)}" />
${ctx.$app.getFeedHTMLMetatags(ctx)} ${ctx.$app.getFeedHTMLMetatags(ctx)}
<link
rel="preload"
href="/dist/fonts/Poppins-400-1.woff2"
as="font"
type="font/woff2"
/>
<link
rel="preload"
href="/dist/fonts/Poppins-400-2.woff2"
as="font"
type="font/woff2"
/>
<script defer src="/dist/bundle.js?v=${start_timestamp}"></script> <script defer src="/dist/bundle.js?v=${start_timestamp}"></script>
${metaImage ? `<meta property="og:image" content="${metaImage}" />` : ""} ${metaImage ? `<meta property="og:image" content="${metaImage}" />` : ""}
${[ ${[

View File

@ -1,15 +1,13 @@
import Router from "@koa/router"; import Router from "@koa/router";
import dgram from "dgram";
import kill from "kill-port"; import kill from "kill-port";
import { Middlewares } from "sealious";
import { default as Koa } from "koa"; import { default as Koa } from "koa";
import installQS from "koa-qs"; import installQS from "koa-qs";
import { Middlewares } from "sealious";
import TheApp from "./app.js"; import TheApp from "./app.js";
import { PORT, SEALIOUS_SANITY } from "./config.js"; import { PORT, SEALIOUS_KILLSWITCH_PORT, SEALIOUS_SANITY } from "./config.js";
import { mainRouter } from "./routes/index.js"; import { mainRouter } from "./routes/index.js";
import { waitForMeilisearch } from "./services/meilisearch.js"; import { waitForMeilisearch } from "./services/meilisearch.js";
export const the_app = new TheApp(); export const the_app = new TheApp();
void (async function () { void (async function () {
@ -34,13 +32,21 @@ void (async function () {
installQS(koa_app); installQS(koa_app);
koa_app.use(Middlewares.handleError()); koa_app.use(Middlewares.handleError());
koa_app.context.$app = the_app; koa_app.context.$app = the_app;
the_app.on("started", () => { the_app.on("started", async () => {
mainRouter(the_app, koa_app, router); mainRouter(the_app, koa_app, router);
}); });
if (SEALIOUS_KILLSWITCH_PORT) {
const server = dgram.createSocket("udp4");
server.on("message", () => {
process.exit(0);
});
server.bind(SEALIOUS_KILLSWITCH_PORT);
}
await the_app.start(); await the_app.start();
koa_app.listen(PORT); koa_app.listen(PORT, "0.0.0.0");
the_app.Logger.info( the_app.Logger.info(
"STARTED", "STARTED",
`App running. URL set in manifest: ${the_app.manifest.base_url}` `App running. URL set in manifest: ${the_app.manifest.base_url}`

View File

@ -1,20 +0,0 @@
import { withProdApp } from "../test_utils/with-prod-app.js";
describe("roles", () => {
it("allows access to users with designated role and denies access to users without it", async () =>
withProdApp(async ({ app }) => {
await app.collections.users.suCreate({
username: "regular-user",
password: "password",
email: "regular@example.com",
roles: [],
});
await app.collections.users.suCreate({
username: "someadmin",
password: "admin-password",
email: "admin@example.com",
roles: [],
});
}));
});

View File

@ -1,40 +0,0 @@
import { withProdApp } from "../test_utils/with-prod-app.js";
import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint.js";
import { HomeURL } from "./urls.js";
import { getBrowser } from "../test_utils/browser-creator.js";
import type { Browser, BrowserContext, Page } from "@playwright/test";
describe("Home webhint", () => {
it("doesn't crash", async function () {
return withProdApp(async ({ base_url, rest_api }) => {
await rest_api.get(HomeURL);
await webhintURL(base_url + HomeURL);
// 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(HomeURL);
// await webhintHTML(response);
});
}).timeout(VERY_LONG_TEST_TIMEOUT);
});
describe("Home", () => {
let page: Page;
let browser: Browser;
let context: BrowserContext;
beforeEach(async () => {
browser = await getBrowser();
context = await browser.newContext();
page = await context.newPage();
});
afterEach(async () => {
await context.close();
});
it("works as expected", async function () {
return withProdApp(async ({ base_url }) => {
await page.goto(base_url + HomeURL);
});
}).timeout(VERY_LONG_TEST_TIMEOUT);
});

View File

@ -1,34 +0,0 @@
import { withProdApp } from "../test_utils/with-prod-app.js";
import { AdminURL } from "./urls.js";
import { getBrowser } from "../test_utils/browser-creator.js";
import type { Browser, BrowserContext, Page } from "@playwright/test";
describe("Admin", () => {
it("doesn't crash", async function () {
return withProdApp(async ({ rest_api }) => {
await rest_api.get(AdminURL);
});
});
});
describe("Admin", () => {
let page: Page;
let browser: Browser;
let context: BrowserContext;
beforeEach(async () => {
browser = await getBrowser();
context = await browser.newContext();
page = await context.newPage();
});
afterEach(async () => {
await context.close();
});
it("works as expected", async function () {
return withProdApp(async ({ base_url }) => {
await page.goto(base_url + AdminURL);
});
});
});

View File

@ -1,34 +0,0 @@
import { withProdApp } from "../../test_utils/with-prod-app.js";
import { ShowFirstRowDemoURL } from "../urls.js";
import { getBrowser } from "../../test_utils/browser-creator.js";
import type { Browser, BrowserContext, Page } from "@playwright/test";
describe("ShowFirstRowDemo", () => {
it("doesn't crash", async function () {
return withProdApp(async ({ rest_api }) => {
await rest_api.get(ShowFirstRowDemoURL);
});
});
});
describe("ShowFirstRowDemo", () => {
let page: Page;
let browser: Browser;
let context: BrowserContext;
beforeEach(async () => {
browser = await getBrowser();
context = await browser.newContext();
page = await context.newPage();
});
afterEach(async () => {
await context.close();
});
it("works as expected", async function () {
return withProdApp(async ({ base_url }) => {
await page.goto(base_url + ShowFirstRowDemoURL);
});
});
});

View File

@ -1,28 +0,0 @@
import type { Browser, BrowserContext, Page } from "@playwright/test";
import { 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();
}
}
export async function getPage(): Promise<{
page: Page;
browser: Browser;
context: BrowserContext;
}> {
const browser = await getBrowser();
const context = await browser.newContext();
const page = await context.newPage();
return { browser, context, page };
}

View File

@ -1,26 +0,0 @@
import type { CollectionItem, TestUtils } from "sealious";
import type TheApp from "../app.js";
import type Users from "../collections/users.js";
type Unpromisify<T> = T extends Promise<infer R> ? R : T;
export function createAUser(app: TheApp, username: string, roles: string[]) {
return app.collections.users.suCreate({
username,
email: `${username}@example.com`,
password: "password",
roles: roles.map((role) => ({ role })),
});
}
export async function createAdmin(
app: TheApp,
rest_api: TestUtils.MockRestApi
): Promise<[CollectionItem<Users>, Unpromisify<ReturnType<typeof rest_api.login>>]> {
const user = await createAUser(app, "super_user", ["admin"]);
const session = await rest_api.login({
username: "super_user",
password: "password",
});
return [user, session];
}

View File

@ -1,84 +0,0 @@
import getPort from "get-port";
import { default as Koa } from "koa";
import { SMTPMailer, TestUtils, Middlewares } from "sealious";
import { v4 as uuid } from "uuid";
import TheApp from "../app.js";
import {
MAILCATCHER_API_PORT,
MAILCATCHER_HOST,
MAILCATCHER_SMTP_PORT,
} from "../config.js";
import { mainRouter } from "../routes/index.js";
import Router from "@koa/router";
const port_numbers = async function* () {
yield await getPort();
};
export async function withProdApp(
callback: (args: {
app: TheApp;
base_url: string;
rest_api: TestUtils.MockRestApi;
mail_api: TestUtils.MailcatcherAPI;
}) => Promise<void>
) {
const app = new TheApp();
const port = (await port_numbers().next()).value;
if (!port) {
console.error("Constant port is empty.");
return;
}
app.config["www-server"].port = port;
app.config.datastore_mongo = {
...app.config.datastore_mongo,
db_name: "sealious-app-test" + uuid(),
};
app.config.logger.level = <const>"none";
app.mailer = new SMTPMailer({
host: MAILCATCHER_HOST,
port: MAILCATCHER_SMTP_PORT,
user: "any",
password: "any",
});
const koa_app = new Koa();
const router = new Router();
koa_app.use(Middlewares.handleError());
koa_app.context.$app = app;
const server = koa_app.listen(port);
app.on("started", () => {
mainRouter(app, koa_app, router);
});
await app.start();
const base_url = `http://127.0.0.1:${port}`;
const mail_api = new TestUtils.MailcatcherAPI(
`http://${MAILCATCHER_HOST}:${MAILCATCHER_API_PORT}`,
app
);
await mail_api.deleteAllInstanceEmails();
async function stop() {
await app.removeAllData();
await app.stop();
server.close();
}
try {
await callback({
app,
base_url,
rest_api: new TestUtils.MockRestApi(base_url),
mail_api,
});
await stop();
} catch (e) {
if (app.status !== "stopped") {
await stop();
}
console.error(e);
throw e;
}
}

View File

@ -1,58 +0,0 @@
const mri = require("mri");
const { spawn } = require("child_process");
const argv = process.argv.slice(2);
const args = mri(argv);
const bin_dir = "./node_modules/.bin/";
const mocha = bin_dir + "mocha";
let mocha_options = [
"--recursive",
"--timeout=100000",
"--require",
"source-map-support/register",
"--exit",
];
if (args["serial"]) {
mocha_options.push("--parallel");
}
if (args["test-report"]) {
mocha_options = [
...mocha_options,
// "--require",
// "ts-node/register",
// "--require",
// "./src/http/type-overrides.ts",
"--reporter",
"xunit",
"--reporter-option",
"output=.xunit",
];
}
const mocha_files = ["dist/**/*.test.js"];
let command = [mocha, ...mocha_options, ...mocha_files];
if (args.debug) {
command = ["node", "inspect", ...command];
}
console.log("spawning mocha...", command.join(" "));
const proc = spawn(command[0], command.slice(1), {
stdio: "inherit",
env: process.env,
});
proc.on("exit", function (code) {
if (args["test-report"]) {
process.exit(0);
} else {
process.exit(code);
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

35
tests/admin.test.ts Normal file
View File

@ -0,0 +1,35 @@
import { test, expect } from "./backend-fixture.js";
test.beforeEach(async ({ page, backendUrl }) => {
await page.goto(backendUrl);
await page.getByRole("link", { name: "Go to Admin" }).click();
await page.getByPlaceholder("text").click();
await page.getByPlaceholder("text").fill("admin");
await page.getByPlaceholder("text").press("Tab");
await page.getByPlaceholder("password").fill("adminadmin");
await page.getByRole("button", { name: "Wyślij" }).click();
});
test("Basic flow of adding a navbar link", async ({ page }) => {
await page.getByRole("link", { name: "Edit Navbar" }).click();
await page.getByRole("link", { name: "Create" }).click();
await page.getByLabel("label").click();
await page.getByLabel("label").fill("Homepage");
await page.getByLabel("href").fill("/");
await page.getByRole("button", { name: "Wyślij" }).click();
await page.getByRole("link", { name: "Homepage" }).click();
await expect(page).toHaveScreenshot();
});
test("Basic flow of creating a page", async ({ page, backendUrl }) => {
await page.getByRole("link", { name: "Edit Pages" }).click();
await page.getByRole("link", { name: "Create" }).click();
await page.getByLabel("url").click();
await page.getByLabel("url").fill("/my-page/");
await page.getByLabel("title").fill("My Page");
await page.getByLabel("heading").fill("My Heading");
await page.getByLabel("description").fill("My Description");
await page.getByRole("button", { name: "Wyślij" }).click();
await page.goto(`${backendUrl}/my-page/`);
await expect(page).toHaveScreenshot();
});

170
tests/backend-fixture.ts Normal file
View File

@ -0,0 +1,170 @@
import { test as base } from "@playwright/test";
import { spawn, spawnSync } from "child_process";
import { v4 as uuid } from "uuid";
import getPort from "get-port";
import * as path from "path";
import * as dgram from "dgram";
import * as http from "http";
import * as fs from "fs/promises";
import { MongoClient } from "mongodb";
import { MONGO_PORT, MONGO_HOST } from "../src/back/config.js";
type TestFixtures = {
backendUrl: string;
setMarkdownValue: (field_name: string, value: string) => Promise<void>;
};
const c8 = "./node_modules/c8/bin/c8.js";
const app_host = process.env.TESTS_RUN_IN_DOCKER
? "test" // the docker name
: "172.17.0.1"; // ip to access localhost from within docker (playwright always runs in docker)
export async function waitForHttpPort({
port,
host = app_host,
timeout = 5000 * 20,
interval = 200,
}: {
port: number;
host?: string;
timeout?: number;
interval?: number;
}): Promise<void> {
const start = Date.now();
return new Promise((resolve, reject) => {
const check = () => {
console.log("Making a request to", { host, port });
const req = http.get({ hostname: host, port, timeout: 1000 }, (res) => {
res.destroy();
resolve(); // Port is ready
});
req.on("error", () => {
if (Date.now() - start > timeout) {
reject(new Error(`Timeout waiting for HTTP port ${port}`));
} else {
setTimeout(check, interval);
}
});
};
check();
});
}
function triggerKillswitch(port: number) {
const client = dgram.createSocket("udp4");
const message = Buffer.from("Hello, UDP!");
const HOST = "127.0.0.1";
client.send(message, port, HOST, () => {
client.close();
});
}
async function dropMongoDB(mongo_db_name: string) {
const client = new MongoClient(`mongodb://${MONGO_HOST}:${MONGO_PORT}`);
try {
await client.connect();
const db = client.db(mongo_db_name);
const result = await db.dropDatabase();
console.log(`Database "${mongo_db_name}" dropped:`, result);
} catch (err) {
console.error("Error dropping database:", err);
} finally {
await client.close();
}
}
export const test = base.extend<TestFixtures>({
backendUrl: async ({}, use, { file, titlePath }) => {
const port = await getPort();
const mongo_db_name = `test-app-${uuid()}`;
const killswitch_port = await getPort();
const coverage_temp_dir = path.join(
"coverage",
"tmp_split",
Date.now().toString()
);
await fs.mkdir(coverage_temp_dir, {
recursive: true,
});
const backendProcess = spawn(
c8,
[
"--src",
"src",
"--reporter",
"none",
"--temp-directory",
coverage_temp_dir,
"--all",
"node",
"dist/back/index.js",
],
{
env: {
...process.env,
SEALIOUS_PORT: `${port}`,
SEALIOUS_MONGO_DB_NAME: mongo_db_name,
...(process.env.TESTS_RUN_IN_DOCKER
? { MEILISEARCH_HOST: `http://meilisearch:7700` }
: {}),
SEALIOUS_KILLSWITCH_PORT: `${killswitch_port}`,
},
stdio: "inherit",
}
);
const onEndPromise = new Promise<void>((resolve) => {
backendProcess.on("exit", async () => {
await dropMongoDB(mongo_db_name);
spawnSync(
c8,
[
"report",
"--temp-directory",
coverage_temp_dir,
"--reporter",
"lcovonly",
"--report-dir",
path.join("coverage", Date.now().toString()),
],
{ stdio: "inherit" }
);
resolve();
});
});
await waitForHttpPort({ port, timeout: 10000 });
await use(`http://${app_host}:${port}`);
triggerKillswitch(killswitch_port);
await onEndPromise;
},
setMarkdownValue: ({ page }, use) => {
use(async (field_name: string, value: string) => {
for (let i = 1; i <= 5; i++) {
try {
await page.getByRole("button", { name: "Preview" }).click();
await page
.locator(`textarea[name="${field_name}"] ~ .CodeMirror`)
.first()
.click({ timeout: 1000 });
await page.getByRole("textbox").press("ControlOrMeta+a");
await page.getByRole("textbox").first().type("Hamster, a dentist");
break;
} catch (e) {
console.error(e);
continue;
}
}
});
},
});
export { expect } from "@playwright/test";

23
tests/global-teardown.ts Normal file
View File

@ -0,0 +1,23 @@
import { glob } from "glob";
import * as fs from "fs/promises";
import * as path from "path";
export default async () => {
console.log("All tests finished. merging coverage information...");
try {
await fs.rm("coverage/tmp", { recursive: true });
} catch (e) {
console.error(
"Couldn't delete the './coverage/tmp' directory - maybe it wasn't there in the first place"
);
}
await fs.mkdir("coverage/tmp", { recursive: true });
let files = await glob("coverage/tmp_split/**/*.json");
await Promise.all(
files.map((file, index) => {
fs.copyFile(file, path.join("coverage", "tmp", path.basename(file)));
})
);
};

16
tests/tekst.test.ts Normal file
View File

@ -0,0 +1,16 @@
import { test, expect } from "./backend-fixture.js";
import { setTimeout as sleep } from "node:timers/promises";
test("Tekst component", async ({ page, backendUrl, setMarkdownValue }) => {
await page.goto(`${backendUrl}/components`);
await page.waitForFunction(() => typeof (window as any).CodeMirror !== "undefined");
await sleep(5000);
await page.locator('select[name="component"]').selectOption("tekst");
await page
.locator(`select[name="$[components][0][args][color]"]`)
.selectOption("accent");
await setMarkdownValue(`$[components][0][args][content]`, "Hamster, a dentist");
await page.getByRole("button", { name: "Preview" }).click();
await expect(page).toHaveScreenshot();
});