Coverage test
Summary: -- Reviewers: #reviewers Subscribers: jenkins-user Differential Revision: https://hub.sealcode.org/D1606
This commit is contained in:
parent
6b470fa709
commit
10ef8cd3d2
2
.gitignore
vendored
2
.gitignore
vendored
@ -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/
|
||||||
|
42
convert-cobertura-to-arcanist.mjs
Normal file
42
convert-cobertura-to-arcanist.mjs
Normal 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));
|
||||||
|
});
|
@ -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
|
||||||
|
@ -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", "--"]
|
||||||
|
|
||||||
|
@ -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
8
npm.sh
@ -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
19803
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@ -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
22
playwright.config.ts
Normal 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}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
@ -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,
|
||||||
|
@ -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";
|
||||||
@ -77,7 +77,7 @@ export default class Pages extends Collection {
|
|||||||
...super.mapFieldsToFeed(),
|
...super.mapFieldsToFeed(),
|
||||||
content: async (ctx, item) => {
|
content: async (ctx, item) => {
|
||||||
const jdd = await getJDD(ctx, item);
|
const jdd = await getJDD(ctx, item);
|
||||||
const result = await tempstream/* HTML */ `${jdd.render()}`;
|
const result = await tempstream /* HTML */ `${jdd.render()}`;
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -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>");
|
|
||||||
}));
|
|
||||||
});
|
|
@ -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;
|
||||||
|
@ -40,23 +40,11 @@ export function defaultHead({
|
|||||||
description: string;
|
description: string;
|
||||||
}): JSX.Element | Readable {
|
}): JSX.Element | Readable {
|
||||||
const origin = ctx.URL.origin;
|
const origin = ctx.URL.origin;
|
||||||
return tempstream/* HTML */ `<title>${title}</title>
|
return tempstream /* HTML */ `<title>${title}</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<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}" />` : ""}
|
||||||
${[
|
${[
|
||||||
|
@ -40,7 +40,7 @@ export default function html({
|
|||||||
controllers.push("refresh-on-ts-changes");
|
controllers.push("refresh-on-ts-changes");
|
||||||
}
|
}
|
||||||
|
|
||||||
return tempstreamAsync/* HTML */ ` <!DOCTYPE html>
|
return tempstreamAsync /* HTML */ ` <!DOCTYPE html>
|
||||||
<html
|
<html
|
||||||
lang="${htmlOptions.language || DEFAULT_HTML_LANG}"
|
lang="${htmlOptions.language || DEFAULT_HTML_LANG}"
|
||||||
class="title--${typeof title == "string" ? toKebabCase(title) : ""}"
|
class="title--${typeof title == "string" ? toKebabCase(title) : ""}"
|
||||||
|
@ -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}`
|
||||||
|
@ -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: [],
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
});
|
|
@ -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);
|
|
||||||
});
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -92,7 +92,7 @@ export default new (class NavbarLinksCRUDEditForm extends Form<typeof fields, vo
|
|||||||
return html({
|
return html({
|
||||||
ctx,
|
ctx,
|
||||||
title: "Edit NavbarLinks",
|
title: "Edit NavbarLinks",
|
||||||
body: tempstream/* HTML */ ` <div class="sealgen-crud-form">
|
body: tempstream /* HTML */ ` <div class="sealgen-crud-form">
|
||||||
<a class="" href="${NavbarLinksCRUDListURL}"
|
<a class="" href="${NavbarLinksCRUDListURL}"
|
||||||
>← Back to navbar-links list</a
|
>← Back to navbar-links list</a
|
||||||
>
|
>
|
||||||
|
@ -50,7 +50,7 @@ export default new (class NavbarLinksCRUDCreateForm extends Form<typeof fields,
|
|||||||
return html({
|
return html({
|
||||||
ctx,
|
ctx,
|
||||||
title: "Create navbar-links",
|
title: "Create navbar-links",
|
||||||
body: tempstream/* HTML */ ` <div class="sealgen-crud-form">
|
body: tempstream /* HTML */ ` <div class="sealgen-crud-form">
|
||||||
<a class="" href="${NavbarLinksCRUDListURL}"
|
<a class="" href="${NavbarLinksCRUDListURL}"
|
||||||
>← Back to navbar-links list</a
|
>← Back to navbar-links list</a
|
||||||
>
|
>
|
||||||
|
@ -134,7 +134,7 @@ export default new (class NavbarLinksCRUDListPage extends SealiousItemListPage<
|
|||||||
ctx: Context,
|
ctx: Context,
|
||||||
fields: { field: string; label?: string }[]
|
fields: { field: string; label?: string }[]
|
||||||
): FlatTemplatable {
|
): FlatTemplatable {
|
||||||
return tempstream/* HTML */ `<thead>
|
return tempstream /* HTML */ `<thead>
|
||||||
<tr>
|
<tr>
|
||||||
${fields.map(({ label, field }) => this.renderHeading(ctx, field, label))}
|
${fields.map(({ label, field }) => this.renderHeading(ctx, field, label))}
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
|
@ -105,7 +105,7 @@ export default new (class PagesCRUDEditForm extends Form<typeof fields, void> {
|
|||||||
return html({
|
return html({
|
||||||
ctx,
|
ctx,
|
||||||
title: "Edit Pages",
|
title: "Edit Pages",
|
||||||
body: tempstream/* HTML */ ` <div class="sealgen-crud-form">
|
body: tempstream /* HTML */ ` <div class="sealgen-crud-form">
|
||||||
<a class="" href="${PagesCRUDListURL}">← Back to pages list</a>
|
<a class="" href="${PagesCRUDListURL}">← Back to pages list</a>
|
||||||
${await super.render(ctx, data, show_field_errors)}
|
${await super.render(ctx, data, show_field_errors)}
|
||||||
</div>`,
|
</div>`,
|
||||||
|
@ -56,7 +56,7 @@ export default new (class PagesCRUDCreateForm extends Form<typeof fields, void>
|
|||||||
return html({
|
return html({
|
||||||
ctx,
|
ctx,
|
||||||
title: "Create pages",
|
title: "Create pages",
|
||||||
body: tempstream/* HTML */ ` <div class="sealgen-crud-form">
|
body: tempstream /* HTML */ ` <div class="sealgen-crud-form">
|
||||||
<a class="" href="${PagesCRUDListURL}">← Back to pages list</a>
|
<a class="" href="${PagesCRUDListURL}">← Back to pages list</a>
|
||||||
${await super.render(ctx, data, show_field_errors)}
|
${await super.render(ctx, data, show_field_errors)}
|
||||||
</div>`,
|
</div>`,
|
||||||
|
@ -146,7 +146,7 @@ export default new (class PagesCRUDListPage extends SealiousItemListPage<
|
|||||||
ctx: Context,
|
ctx: Context,
|
||||||
fields: { field: string; label?: string }[]
|
fields: { field: string; label?: string }[]
|
||||||
): FlatTemplatable {
|
): FlatTemplatable {
|
||||||
return tempstream/* HTML */ `<thead>
|
return tempstream /* HTML */ `<thead>
|
||||||
<tr>
|
<tr>
|
||||||
${fields.map(({ label, field }) => this.renderHeading(ctx, field, label))}
|
${fields.map(({ label, field }) => this.renderHeading(ctx, field, label))}
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -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 };
|
|
||||||
}
|
|
@ -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];
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
58
test.cjs
58
test.cjs
@ -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 |
BIN
tests/__screenshots/tekst.test.ts/Tekst-component-1.png
Normal file
BIN
tests/__screenshots/tekst.test.ts/Tekst-component-1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
35
tests/admin.test.ts
Normal file
35
tests/admin.test.ts
Normal 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
170
tests/backend-fixture.ts
Normal 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
23
tests/global-teardown.ts
Normal 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
16
tests/tekst.test.ts
Normal 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();
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user