Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8914d4bcb |
+42
-143
@@ -1,21 +1,16 @@
|
|||||||
name: CI
|
name: CI compile & test & build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
backend_image:
|
|
||||||
description: 'Backend image to use in E2E tests (optional, default: ghcr.io/morbalint/kemkas-backend:b8565f8e)'
|
|
||||||
required: false
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
unit-test-and-lint:
|
ci-fe:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Node.js 24.x
|
- name: Setup Node.js 24.x
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
@@ -37,143 +32,47 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd ./frontend
|
cd ./frontend
|
||||||
yarn run test
|
yarn run test
|
||||||
- name: Yarn Lint
|
|
||||||
run: |
|
|
||||||
cd ./frontend
|
|
||||||
yarn run lint
|
|
||||||
|
|
||||||
build-fe:
|
docker-fe-build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
attestations: write
|
|
||||||
id-token: write
|
|
||||||
outputs:
|
outputs:
|
||||||
image_sha_tag: ${{ steps.image-tag.outputs.image_sha_tag }}
|
image_tag: ${{ steps.get_image_tag.outputs.image_tag }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
- uses: docker/setup-buildx-action@v4
|
- uses: docker/setup-buildx-action@v3
|
||||||
- name: Log in to GitHub Container Registry
|
- name: Log in to GitHub Container Registry
|
||||||
uses: docker/login-action@v4
|
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||||
with:
|
- id: get_image_tag
|
||||||
registry: ghcr.io
|
run: |
|
||||||
username: ${{ github.actor }}
|
echo "GITHUB_SHA_SHORT=$(echo ${GITHUB_SHA::8})" >> $GITHUB_ENV
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
echo "ghcr.io/${{ github.repository_owner }}/kemkas-frontend:${GITHUB_SHA::8}"
|
||||||
- name: Compute frontend image tag
|
echo "image_tag=ghcr.io/${{ github.repository_owner }}/kemkas-fe:${GITHUB_SHA::8}" >> $GITHUB_OUTPUT
|
||||||
id: image-tag
|
- uses: docker/build-push-action@v5
|
||||||
run: |
|
with:
|
||||||
image_repo="ghcr.io/${{ github.repository_owner }}/kemkas-frontend"
|
context: ./frontend/
|
||||||
sha_tag="${image_repo}:${GITHUB_SHA::8}"
|
file: ./frontend/Dockerfile
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
ghcr.io/${{ github.repository_owner }}/kemkas-frontend:${{ env.GITHUB_SHA_SHORT }}
|
||||||
|
|
||||||
tags="$sha_tag"
|
# No longer used, and token expired.
|
||||||
if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then
|
# deploy-to-beta:
|
||||||
tags+=$'\n'"${image_repo}:latest"
|
# runs-on: ubuntu-latest
|
||||||
fi
|
# needs:
|
||||||
|
# - docker-fe-build
|
||||||
echo "image_sha_tag=${sha_tag}" >> "$GITHUB_OUTPUT"
|
# steps:
|
||||||
{
|
# - uses: actions/checkout@v4
|
||||||
echo "tags<<EOF"
|
# with:
|
||||||
echo "$tags"
|
# repository: ${{ github.repository_owner }}/kemkas-deployment
|
||||||
echo "EOF"
|
# ref: refs/heads/main
|
||||||
} >> "$GITHUB_OUTPUT"
|
# ssh-key: ${{ secrets.DEPLOYMENT_REPO_DEPLOY_KEY }}
|
||||||
- name: Build and push frontend image
|
# - uses: mikefarah/yq@v4
|
||||||
uses: docker/build-push-action@v7
|
# with:
|
||||||
with:
|
# cmd: yq -i '.spec.template.spec.containers[0].image = "${{ needs.docker-fe-build.outputs.image_tag }}"' ./kemkas-beta/kemkas-fe-deployment.yaml
|
||||||
context: ./frontend/
|
# - run: |
|
||||||
file: ./frontend/Dockerfile
|
# git config --add user.email "beta-fe-deployment-bot@kemkas.hu"
|
||||||
cache-from: type=gha
|
# git config --add user.name "Deployment Bot Beta FE"
|
||||||
cache-to: type=gha,mode=max
|
# git commit -a -m "deploy ${{ needs.docker-fe-build.outputs.image_tag }}"
|
||||||
push: true
|
# git push
|
||||||
tags: |
|
|
||||||
${{ steps.image-tag.outputs.tags }}
|
|
||||||
|
|
||||||
build-e2e:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
attestations: write
|
|
||||||
id-token: write
|
|
||||||
outputs:
|
|
||||||
image_sha_tag: ${{ steps.image-tag.outputs.image_sha_tag }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: docker/setup-buildx-action@v4
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v4
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Compute e2e image tag
|
|
||||||
id: image-tag
|
|
||||||
run: |
|
|
||||||
image_repo="ghcr.io/${{ github.repository_owner }}/kemkas-e2e"
|
|
||||||
sha_tag="${image_repo}:${GITHUB_SHA::8}"
|
|
||||||
|
|
||||||
tags="$sha_tag"
|
|
||||||
if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then
|
|
||||||
tags+=$'\n'"${image_repo}:latest"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "image_sha_tag=${sha_tag}" >> "$GITHUB_OUTPUT"
|
|
||||||
{
|
|
||||||
echo "tags<<EOF"
|
|
||||||
echo "$tags"
|
|
||||||
echo "EOF"
|
|
||||||
} >> "$GITHUB_OUTPUT"
|
|
||||||
- name: Build and push e2e image
|
|
||||||
uses: docker/build-push-action@v7
|
|
||||||
with:
|
|
||||||
context: ./e2e/
|
|
||||||
file: ./e2e/Dockerfile
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
${{ steps.image-tag.outputs.tags }}
|
|
||||||
|
|
||||||
run-e2e:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs:
|
|
||||||
- build-fe
|
|
||||||
- build-e2e
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: read
|
|
||||||
env:
|
|
||||||
BACKEND_IMAGE: ${{ github.event.inputs.backend_image || 'ghcr.io/morbalint/kemkas-backend:b8565f8e' }}
|
|
||||||
FRONTEND_IMAGE: ${{ needs.build-fe.outputs.image_sha_tag }}
|
|
||||||
E2E_IMAGE: ${{ needs.build-e2e.outputs.image_sha_tag }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Setup docker compose
|
|
||||||
uses: docker/setup-compose-action@v2
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v4
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Pull images for test run
|
|
||||||
run: docker compose -f e2e.docker-compose.yaml pull
|
|
||||||
- name: Run Docker Compose E2E tests
|
|
||||||
run: docker compose -f e2e.docker-compose.yaml up --no-build --abort-on-container-exit --exit-code-from e2e e2e
|
|
||||||
- name: Upload Playwright HTML report
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: playwright-report
|
|
||||||
path: e2e/playwright-report
|
|
||||||
if-no-files-found: ignore
|
|
||||||
- name: Upload Playwright test results
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: playwright-test-results
|
|
||||||
path: e2e/test-results
|
|
||||||
if-no-files-found: ignore
|
|
||||||
- name: Tear down Docker Compose stack
|
|
||||||
if: always()
|
|
||||||
run: docker compose -f e2e.docker-compose.yaml down -v --remove-orphans
|
|
||||||
|
|||||||
@@ -32,11 +32,3 @@ yarn-error.log*
|
|||||||
obj/
|
obj/
|
||||||
bin/
|
bin/
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
# IDEs and editors
|
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
|
|
||||||
# playwright
|
|
||||||
/e2e/playwright-report/
|
|
||||||
/e2e/test-results/
|
|
||||||
@@ -25,13 +25,3 @@ dotnet dev-certs https --format PEM --no-password -ep ~/.aspnet/https/kemkas.pem
|
|||||||
```
|
```
|
||||||
|
|
||||||
Later start the docker compose first and the project launch settings after the DB is up and running.
|
Later start the docker compose first and the project launch settings after the DB is up and running.
|
||||||
|
|
||||||
## E2E with Docker Compose
|
|
||||||
|
|
||||||
For a fully containerized end-to-end run (tagged backend image + freshly built frontend/e2e images):
|
|
||||||
|
|
||||||
```shell
|
|
||||||
docker compose -f e2e.docker-compose.yaml up --build --abort-on-container-exit --exit-code-from e2e e2e
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `BACKEND_IMAGE` to select a specific backend tag, and note this compose stack auto-generates TLS certificates for the nginx proxy.
|
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
services:
|
|
||||||
certs:
|
|
||||||
image: alpine:3.21
|
|
||||||
volumes:
|
|
||||||
- tls-certs:/certs
|
|
||||||
- ./scripts/generate-e2e-certs.sh:/scripts/generate-e2e-certs.sh:ro
|
|
||||||
command: ["sh", "/scripts/generate-e2e-certs.sh"]
|
|
||||||
|
|
||||||
db:
|
|
||||||
image: postgres:16.13-alpine3.23
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: "kemkas"
|
|
||||||
POSTGRES_PASSWORD: "kemkas-dev42"
|
|
||||||
volumes:
|
|
||||||
- db-data:/var/lib/postgresql/data
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U kemkas -d postgres"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 20
|
|
||||||
start_period: 5s
|
|
||||||
|
|
||||||
backend:
|
|
||||||
image: ${BACKEND_IMAGE:-ghcr.io/morbalint/kemkas-backend:b8565f8e}
|
|
||||||
environment:
|
|
||||||
ConnectionStrings__DefaultConnection: "Server=db;User Id=kemkas;Password=kemkas-dev42"
|
|
||||||
Email__DomainName: "mail.kemkas.hu"
|
|
||||||
Email__ApiKey: ""
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
|
|
||||||
frontend:
|
|
||||||
image: ${FRONTEND_IMAGE:-ghcr.io/morbalint/kemkas-frontend:local}
|
|
||||||
build:
|
|
||||||
context: ./frontend
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
|
|
||||||
proxy:
|
|
||||||
image: nginx:1.29.7-alpine3.23
|
|
||||||
ports:
|
|
||||||
- "8000:8000"
|
|
||||||
volumes:
|
|
||||||
- ./e2e.nginx.conf:/etc/nginx/nginx.conf:ro
|
|
||||||
- tls-certs:/etc/nginx/certs:ro
|
|
||||||
depends_on:
|
|
||||||
certs:
|
|
||||||
condition: service_completed_successfully
|
|
||||||
backend:
|
|
||||||
condition: service_started
|
|
||||||
frontend:
|
|
||||||
condition: service_started
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "wget -q --spider --no-check-certificate https://127.0.0.1:8000 || exit 1"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 20
|
|
||||||
start_period: 5s
|
|
||||||
|
|
||||||
e2e:
|
|
||||||
image: ${E2E_IMAGE:-ghcr.io/morbalint/kemkas-e2e:local}
|
|
||||||
build:
|
|
||||||
context: ./e2e
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
environment:
|
|
||||||
E2E_BASE_URL: https://proxy:8000
|
|
||||||
E2E_USE_WEBSERVER: "false"
|
|
||||||
E2E_TRUSTED_CERT_PATH: /certs/rootCA.pem
|
|
||||||
E2E_WAIT_FOR_URL: https://proxy:8000
|
|
||||||
volumes:
|
|
||||||
- tls-certs:/certs:ro
|
|
||||||
- ./e2e/playwright-report:/work/playwright-report
|
|
||||||
- ./e2e/test-results:/work/test-results
|
|
||||||
depends_on:
|
|
||||||
certs:
|
|
||||||
condition: service_completed_successfully
|
|
||||||
proxy:
|
|
||||||
condition: service_healthy
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
db-data:
|
|
||||||
tls-certs:
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
user nginx;
|
|
||||||
worker_processes auto;
|
|
||||||
|
|
||||||
error_log /var/log/nginx/error.log notice;
|
|
||||||
pid /var/run/nginx.pid;
|
|
||||||
|
|
||||||
|
|
||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
http {
|
|
||||||
include /etc/nginx/mime.types;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
|
|
||||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
|
||||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
|
||||||
|
|
||||||
access_log /var/log/nginx/access.log main;
|
|
||||||
|
|
||||||
sendfile on;
|
|
||||||
keepalive_timeout 65;
|
|
||||||
|
|
||||||
upstream frontend {
|
|
||||||
server frontend:80;
|
|
||||||
}
|
|
||||||
|
|
||||||
upstream backend {
|
|
||||||
server backend:8080;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 8000 ssl;
|
|
||||||
server_name localhost;
|
|
||||||
|
|
||||||
ssl_certificate /etc/nginx/certs/kemkas.pem;
|
|
||||||
ssl_certificate_key /etc/nginx/certs/kemkas.key;
|
|
||||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
|
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://frontend;
|
|
||||||
}
|
|
||||||
|
|
||||||
error_page 500 502 503 504 /50x.html;
|
|
||||||
location = /50x.html {
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /Identity/ {
|
|
||||||
proxy_pass_request_headers on;
|
|
||||||
proxy_set_header X-Forwarded-For "$proxy_add_x_forwarded_for";
|
|
||||||
proxy_set_header X-Forwarded-Proto "$scheme";
|
|
||||||
proxy_set_header X-Forwarded-Host "$http_host";
|
|
||||||
proxy_pass http://backend;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass_request_headers on;
|
|
||||||
proxy_set_header X-Forwarded-For "$proxy_add_x_forwarded_for";
|
|
||||||
proxy_set_header X-Forwarded-Proto "$scheme";
|
|
||||||
proxy_set_header X-Forwarded-Host "$http_host";
|
|
||||||
proxy_pass http://backend;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /signin- {
|
|
||||||
proxy_pass_request_headers on;
|
|
||||||
proxy_set_header X-Forwarded-For "$proxy_add_x_forwarded_for";
|
|
||||||
proxy_set_header X-Forwarded-Proto "$scheme";
|
|
||||||
proxy_set_header X-Forwarded-Host "$http_host";
|
|
||||||
proxy_pass http://backend;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /lib/ {
|
|
||||||
proxy_pass_request_headers on;
|
|
||||||
proxy_pass http://backend;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /js/ {
|
|
||||||
proxy_pass_request_headers on;
|
|
||||||
proxy_pass http://backend;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /css/ {
|
|
||||||
proxy_pass_request_headers on;
|
|
||||||
proxy_pass http://backend;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /img/ {
|
|
||||||
proxy_pass_request_headers on;
|
|
||||||
proxy_pass http://backend;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
node_modules
|
|
||||||
playwright-report
|
|
||||||
test-results
|
|
||||||
.vscode
|
|
||||||
.git
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
FROM mcr.microsoft.com/playwright:v1.59.1-noble
|
|
||||||
|
|
||||||
LABEL org.opencontainers.image.source = "https://github.com/morbalint/kemkas"
|
|
||||||
|
|
||||||
WORKDIR /work
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends libnss3-tools \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
COPY package.json yarn.lock ./
|
|
||||||
RUN yarn install --frozen-lockfile
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
RUN chmod +x /work/docker-entrypoint.sh
|
|
||||||
|
|
||||||
ENTRYPOINT ["/work/docker-entrypoint.sh"]
|
|
||||||
CMD ["yarn", "test"]
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
# E2E tests (Playwright)
|
|
||||||
|
|
||||||
This folder contains browser-level end-to-end tests for public, backend-independent user flows.
|
|
||||||
|
|
||||||
## Current scope
|
|
||||||
|
|
||||||
- Guest redirect from `/` to `/2e/karakter`
|
|
||||||
- Guest header links (`Register`, `Login`)
|
|
||||||
- Public legal pages (`/privacy.html`, `/ogl.html`, `/aelf.html`)
|
|
||||||
|
|
||||||
## Run locally
|
|
||||||
|
|
||||||
From `frontend/`:
|
|
||||||
|
|
||||||
- Install Playwright browser binaries:
|
|
||||||
- `yarn test:e2e:install`
|
|
||||||
- Run tests:
|
|
||||||
- `yarn test:e2e`
|
|
||||||
- Run headed mode:
|
|
||||||
- `yarn test:e2e:headed`
|
|
||||||
- Run interactive UI mode:
|
|
||||||
- `yarn test:e2e:ui`
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Tests use the config in `../e2e/playwright.config.ts`.
|
|
||||||
- The Playwright web server starts Vite on `http://127.0.0.1:4173`.
|
|
||||||
- Tests mock `GET /api/User/me` so they stay deterministic without the backend.
|
|
||||||
|
|
||||||
## Run with Docker Compose
|
|
||||||
|
|
||||||
This mode runs against a full containerized stack:
|
|
||||||
|
|
||||||
- tagged backend image (`BACKEND_IMAGE`)
|
|
||||||
- supporting services (`postgres`, `nginx` proxy)
|
|
||||||
- freshly built `frontend` image
|
|
||||||
- freshly built `e2e` image
|
|
||||||
|
|
||||||
Prerequisites:
|
|
||||||
|
|
||||||
- Docker with Compose
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
docker compose -f e2e.docker-compose.yaml up --build --abort-on-container-exit --exit-code-from e2e e2e
|
|
||||||
```
|
|
||||||
|
|
||||||
Run with a different backend tag:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
BACKEND_IMAGE=ghcr.io/morbalint/kemkas-backend:<tag> \
|
|
||||||
docker compose -f e2e.docker-compose.yaml up --build --abort-on-container-exit --exit-code-from e2e e2e
|
|
||||||
```
|
|
||||||
|
|
||||||
The `certs` service generates a local CA and nginx server certificate in a shared volume.
|
|
||||||
The `e2e` image imports that CA (`/certs/rootCA.pem`) into both the system trust store and Chromium's NSS store before running tests.
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
trusted_cert_path="${E2E_TRUSTED_CERT_PATH:-}"
|
|
||||||
if [[ -n "$trusted_cert_path" ]]; then
|
|
||||||
if [[ ! -f "$trusted_cert_path" ]]; then
|
|
||||||
echo "Trusted certificate not found at: $trusted_cert_path" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
install -m 0644 -D "$trusted_cert_path" /usr/local/share/ca-certificates/kemkas-proxy.crt
|
|
||||||
update-ca-certificates
|
|
||||||
|
|
||||||
if command -v certutil >/dev/null 2>&1; then
|
|
||||||
for candidate_home in /root /home/pwuser; do
|
|
||||||
if [[ -d "$candidate_home" ]]; then
|
|
||||||
nss_db_dir="$candidate_home/.pki/nssdb"
|
|
||||||
mkdir -p "$nss_db_dir"
|
|
||||||
certutil -d "sql:$nss_db_dir" -N --empty-password >/dev/null 2>&1 || true
|
|
||||||
certutil -d "sql:$nss_db_dir" -D -n kemkas-proxy >/dev/null 2>&1 || true
|
|
||||||
certutil -d "sql:$nss_db_dir" -A -t "C,," -n kemkas-proxy -i "$trusted_cert_path"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "${E2E_WAIT_FOR_URL:-}" ]]; then
|
|
||||||
echo "Waiting for target URL: ${E2E_WAIT_FOR_URL}"
|
|
||||||
for attempt in {1..60}; do
|
|
||||||
if curl --silent --show-error --fail "${E2E_WAIT_FOR_URL}" >/dev/null; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$attempt" -eq 60 ]]; then
|
|
||||||
echo "Timed out waiting for ${E2E_WAIT_FOR_URL}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$@"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "kemkas-e2e",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"test": "playwright test",
|
|
||||||
"test:headed": "playwright test --headed",
|
|
||||||
"test:ui": "playwright test --ui",
|
|
||||||
"install:browsers": "playwright install chromium"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@playwright/test": "1.59.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test';
|
|
||||||
import path from 'node:path';
|
|
||||||
|
|
||||||
const port = Number(process.env.E2E_PORT ?? 4173);
|
|
||||||
const baseURL = process.env.E2E_BASE_URL ?? `http://127.0.0.1:${port}`;
|
|
||||||
const useWebServer = process.env.E2E_USE_WEBSERVER !== 'false';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
testDir: path.join(__dirname, 'tests'),
|
|
||||||
outputDir: path.join(__dirname, 'test-results'),
|
|
||||||
fullyParallel: true,
|
|
||||||
forbidOnly: Boolean(process.env.CI),
|
|
||||||
retries: process.env.CI ? 2 : 0,
|
|
||||||
workers: process.env.CI ? 2 : undefined,
|
|
||||||
timeout: 30_000,
|
|
||||||
reporter: [
|
|
||||||
['list'],
|
|
||||||
['html', { outputFolder: path.join(__dirname, 'playwright-report'), open: 'never' }],
|
|
||||||
],
|
|
||||||
use: {
|
|
||||||
baseURL,
|
|
||||||
trace: 'on-first-retry',
|
|
||||||
screenshot: 'only-on-failure',
|
|
||||||
video: 'retain-on-failure',
|
|
||||||
},
|
|
||||||
...(useWebServer
|
|
||||||
? {
|
|
||||||
webServer: {
|
|
||||||
command: `node ./node_modules/vite/bin/vite.js --host 127.0.0.1 --port ${port}`,
|
|
||||||
cwd: path.resolve(__dirname, '../frontend'),
|
|
||||||
port,
|
|
||||||
timeout: 120_000,
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
name: 'chromium',
|
|
||||||
use: { ...devices['Desktop Chrome'] },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
|
||||||
|
|
||||||
async function mockGuestSession(page: import('@playwright/test').Page): Promise<void> {
|
|
||||||
await page.route('**/api/User/me', async (route) => {
|
|
||||||
await route.fulfill({
|
|
||||||
status: 200,
|
|
||||||
contentType: 'text/plain; charset=utf-8',
|
|
||||||
body: '',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
test('guest users are redirected to 2e character creation', async ({ page }) => {
|
|
||||||
await mockGuestSession(page);
|
|
||||||
|
|
||||||
await page.goto('/');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/2e\/karakter$/);
|
|
||||||
await expect(page.getByRole('heading', { name: 'Karakter létrehozása' })).toBeVisible();
|
|
||||||
await expect(page.getByRole('button', { name: /Mentés/ })).toBeVisible();
|
|
||||||
await expect(page.getByRole('button', { name: /PDF/ })).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('guest navigation shows Register and Login links', async ({ page }) => {
|
|
||||||
await mockGuestSession(page);
|
|
||||||
|
|
||||||
await page.goto('/2e/karakter');
|
|
||||||
|
|
||||||
const registerLink = page.getByRole('link', { name: 'Register' });
|
|
||||||
const loginLink = page.getByRole('link', { name: 'Login' });
|
|
||||||
|
|
||||||
await expect(registerLink).toBeVisible();
|
|
||||||
await expect(registerLink).toHaveAttribute('href', '/Identity/Account/Register');
|
|
||||||
await expect(loginLink).toBeVisible();
|
|
||||||
await expect(loginLink).toHaveAttribute('href', '/Identity/Account/Login');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('public legal pages are served', async ({ page }) => {
|
|
||||||
await page.goto('/privacy.html');
|
|
||||||
await expect(page.getByRole('heading', { name: 'Privacy Policy', level: 1 })).toBeVisible();
|
|
||||||
|
|
||||||
await page.goto('/ogl.html');
|
|
||||||
await expect(page).toHaveTitle(/Open Game License v1\.0a/);
|
|
||||||
await expect(page.locator('body')).toContainText('OPEN GAME LICENSE Version 1.0a');
|
|
||||||
|
|
||||||
await page.goto('/aelf.html');
|
|
||||||
await expect(page).toHaveTitle(/AELF Open License version 1\.0a/);
|
|
||||||
await expect(page.locator('body')).toContainText('AELF OPEN LICENSE VERSION 1.0a');
|
|
||||||
});
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
|
||||||
# yarn lockfile v1
|
|
||||||
|
|
||||||
|
|
||||||
"@playwright/test@1.59.1":
|
|
||||||
version "1.59.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.59.1.tgz#5c4d38eac84a61527af466602ae20277685a02d6"
|
|
||||||
integrity sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==
|
|
||||||
dependencies:
|
|
||||||
playwright "1.59.1"
|
|
||||||
|
|
||||||
fsevents@2.3.2:
|
|
||||||
version "2.3.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
|
||||||
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
|
||||||
|
|
||||||
playwright-core@1.59.1:
|
|
||||||
version "1.59.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.59.1.tgz#d8a2b28bcb8f2bd08ef3df93b02ae83c813244b2"
|
|
||||||
integrity sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==
|
|
||||||
|
|
||||||
playwright@1.59.1:
|
|
||||||
version "1.59.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.59.1.tgz#f7b0ca61637ae25264cec370df671bbe1f368a4a"
|
|
||||||
integrity sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==
|
|
||||||
dependencies:
|
|
||||||
playwright-core "1.59.1"
|
|
||||||
optionalDependencies:
|
|
||||||
fsevents "2.3.2"
|
|
||||||
@@ -12,7 +12,5 @@ RUN yarn build
|
|||||||
|
|
||||||
FROM nginx:1.29.7-alpine3.23 AS proxy
|
FROM nginx:1.29.7-alpine3.23 AS proxy
|
||||||
|
|
||||||
LABEL org.opencontainers.image.source = "https://github.com/morbalint/kemkas"
|
|
||||||
|
|
||||||
COPY --from=builder "/src/dist" "/usr/share/nginx/html"
|
COPY --from=builder "/src/dist" "/usr/share/nginx/html"
|
||||||
COPY ./nginx.conf /etc/nginx/nginx.conf
|
COPY ./nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import tsParser from '@typescript-eslint/parser';
|
|
||||||
|
|
||||||
export default [
|
|
||||||
{
|
|
||||||
ignores: ['dist/**', 'build/**', 'node_modules/**'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ['src/**/*.{ts,tsx}', 'vite.config.mts', 'vitest.setup.ts'],
|
|
||||||
languageOptions: {
|
|
||||||
parser: tsParser,
|
|
||||||
ecmaVersion: 'latest',
|
|
||||||
sourceType: 'module',
|
|
||||||
parserOptions: {
|
|
||||||
ecmaFeatures: {
|
|
||||||
jsx: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'no-debugger': 'error',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"@types/node": "^24.0.0",
|
"@types/node": "^24.0.0",
|
||||||
"@types/react": "^18.3.2",
|
"@types/react": "^18.3.2",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^18.2.22",
|
||||||
"axios": "^1.13.5",
|
"axios": "^1.7.4",
|
||||||
"bootstrap": "^5.3.2",
|
"bootstrap": "^5.3.2",
|
||||||
"downloadjs": "^1.4.7",
|
"downloadjs": "^1.4.7",
|
||||||
"merge": "^2.1.1",
|
"merge": "^2.1.1",
|
||||||
@@ -40,10 +40,8 @@
|
|||||||
"workbox-streams": "^7.1.0"
|
"workbox-streams": "^7.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/parser": "^8.31.1",
|
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"@types/downloadjs": "^1.4.4",
|
"@types/downloadjs": "^1.4.4",
|
||||||
"eslint": "^9.24.0",
|
|
||||||
"jsdom": "^25.0.1",
|
"jsdom": "^25.0.1",
|
||||||
"less": "^4.2.0",
|
"less": "^4.2.0",
|
||||||
"vite": "^5.4.10",
|
"vite": "^5.4.10",
|
||||||
@@ -53,14 +51,9 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint \"src/**/*.{ts,tsx}\" vite.config.mts vitest.setup.ts",
|
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest"
|
||||||
"test:e2e": "yarn --cwd ../e2e test",
|
|
||||||
"test:e2e:headed": "yarn --cwd ../e2e test:headed",
|
|
||||||
"test:e2e:ui": "yarn --cwd ../e2e test:ui",
|
|
||||||
"test:e2e:install": "yarn --cwd ../e2e install:browsers"
|
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
|||||||
@@ -3,11 +3,6 @@ import { render } from '@testing-library/react';
|
|||||||
import { vi } from 'vitest';
|
import { vi } from 'vitest';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|
||||||
vi.mock('./shared/api/http', () => ({
|
|
||||||
getText: vi.fn(async () => ''),
|
|
||||||
getJson: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('react-router-dom', async () => {
|
vi.mock('react-router-dom', async () => {
|
||||||
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
|
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
|
||||||
|
|
||||||
@@ -19,5 +14,5 @@ vi.mock('react-router-dom', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('renders App', () => {
|
test('renders App', () => {
|
||||||
render(<App />);
|
render(<App faro={undefined}/>);
|
||||||
});
|
});
|
||||||
|
|||||||
+105
-112
@@ -1,23 +1,18 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import {Faro} from "@grafana/faro-web-sdk";
|
import {Faro} from "@grafana/faro-web-sdk";
|
||||||
import {createBrowserRouter, LoaderFunctionArgs, Navigate, Outlet, redirect, RouterProvider} from "react-router-dom";
|
import {createBrowserRouter, Outlet, redirect, RouterProvider} from "react-router-dom";
|
||||||
import CharacterList, {CharacterListItemDto} from "./shared/pages/CharacterList";
|
import CreateCharacter from "./first-edition/pages/CreateCharacter";
|
||||||
|
import CharacterList from "./shared/pages/CharacterList";
|
||||||
import ErrorBoundary from "./shared/ErrorBoundary";
|
import ErrorBoundary from "./shared/ErrorBoundary";
|
||||||
import Header from "./shared/Header";
|
import Header from "./shared/Header";
|
||||||
import Footer from "./shared/Footer";
|
import Footer from "./shared/Footer";
|
||||||
|
import CreateCharacter2E from "./second-edition/pages/CreateCharacter2E";
|
||||||
import store, {AppDispatch, RootState} from './store'
|
import store, {AppDispatch, RootState} from './store'
|
||||||
import { Provider } from "react-redux"
|
import { Provider } from "react-redux"
|
||||||
import { useSelector, useDispatch } from 'react-redux'
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
import {fetchCurrentUser, userSelector} from './shared/domain-models/userSlice'
|
import {load, setUser, unsetUser, userSelector} from './shared/domain-models/userSlice'
|
||||||
import {setCharacter} from "./second-edition/domain-models/characterSlice";
|
import {setCharacter} from "./second-edition/domain-models/characterSlice";
|
||||||
import {getJson} from "./shared/api/http";
|
|
||||||
import {KarakterInputs} from "./first-edition/domain-models/karakter";
|
|
||||||
import {Karakter2E} from "./second-edition/domain-models/karakter2E";
|
|
||||||
const CreateCharacter = React.lazy(() => import("./first-edition/pages/CreateCharacter"))
|
|
||||||
const CreateCharacter2E = React.lazy(() => import("./second-edition/pages/CreateCharacter2E"))
|
|
||||||
|
|
||||||
const FaroContext = React.createContext<Faro | undefined>(undefined)
|
|
||||||
|
|
||||||
function RootLayout() {
|
function RootLayout() {
|
||||||
return (
|
return (
|
||||||
@@ -31,118 +26,116 @@ function RootLayout() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function IndexRedirect() {
|
function Router(props: {faro?: Faro}) {
|
||||||
|
const dispatch = useDispatch.withTypes<AppDispatch>()()
|
||||||
const fetchedUser = useSelector.withTypes<RootState>()(userSelector)
|
const fetchedUser = useSelector.withTypes<RootState>()(userSelector)
|
||||||
|
|
||||||
if (fetchedUser.state !== "finished") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Navigate replace to={fetchedUser.email == null ? "/2e/karakter" : "/karaktereim"} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function CreateCharacter1ERoute() {
|
|
||||||
const faro = React.useContext(FaroContext)
|
|
||||||
return (
|
|
||||||
<React.Suspense fallback={<div className="p-3">Betoltes...</div>}>
|
|
||||||
{faro ? <CreateCharacter faro={faro} /> : <CreateCharacter />}
|
|
||||||
</React.Suspense>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CreateCharacter2ERoute() {
|
|
||||||
const faro = React.useContext(FaroContext)
|
|
||||||
return (
|
|
||||||
<React.Suspense fallback={<div className="p-3">Betoltes...</div>}>
|
|
||||||
{faro ? <CreateCharacter2E faro={faro} /> : <CreateCharacter2E />}
|
|
||||||
</React.Suspense>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function UserBootstrap() {
|
|
||||||
const dispatch = useDispatch.withTypes<AppDispatch>()()
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
void dispatch(fetchCurrentUser())
|
if (fetchedUser.state !== "not-started") {
|
||||||
}, [dispatch])
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
let isCancelled = false;
|
||||||
|
dispatch(load())
|
||||||
|
fetch(`${window.location.origin}/api/User/me`)
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
console.log(response)
|
||||||
|
})
|
||||||
|
.then(userNameResponse => {
|
||||||
|
if (isCancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userNameResponse && userNameResponse.length > 0) {
|
||||||
|
dispatch(setUser(userNameResponse))
|
||||||
|
} else {
|
||||||
|
dispatch(unsetUser())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (isCancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(unsetUser())
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isCancelled = true;
|
||||||
|
}
|
||||||
|
}, [dispatch, fetchedUser.state])
|
||||||
|
|
||||||
|
const router = React.useMemo(() => createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <RootLayout />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
loader: () => fetchedUser.email == null ? redirect("/2e/karakter") : redirect("/karaktereim")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "karaktereim",
|
||||||
|
element: <CharacterList />,
|
||||||
|
loader: () => fetch(`${window.location.origin}/api/Character`),
|
||||||
|
ErrorBoundary: ErrorBoundary,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "1e",
|
||||||
|
loader: () => redirect("/1e/karakter")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "1e/karakter",
|
||||||
|
element: <CreateCharacter faro={props.faro} />,
|
||||||
|
ErrorBoundary: ErrorBoundary,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "1e/karakter/:id",
|
||||||
|
element: <CreateCharacter faro={props.faro}/>,
|
||||||
|
loader: args => fetch(`${window.location.origin}/api/Character1E/${args.params.id}`),
|
||||||
|
ErrorBoundary: ErrorBoundary,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "2e",
|
||||||
|
loader: () => redirect("/2e/karakter")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "2e/karakter",
|
||||||
|
element: <CreateCharacter2E faro={props.faro}/>,
|
||||||
|
ErrorBoundary: ErrorBoundary,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "2e/karakter/:id",
|
||||||
|
element: <CreateCharacter2E faro={props.faro}/>,
|
||||||
|
loader: async args => {
|
||||||
|
const response = await fetch(`${window.location.origin}/api/Character2E/${args.params.id}`)
|
||||||
|
if (response.ok) {
|
||||||
|
const loaded2Echaracter = await response.json()
|
||||||
|
dispatch(setCharacter(loaded2Echaracter))
|
||||||
|
return loaded2Echaracter;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isPublic: false,
|
||||||
|
error: response.statusText,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
ErrorBoundary: ErrorBoundary,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]), [dispatch, fetchedUser.email, props.faro]);
|
||||||
|
|
||||||
|
return <RouterProvider router={router}/>
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadCharacterList = () => getJson<CharacterListItemDto[]>("/api/Character")
|
|
||||||
|
|
||||||
const loadCharacter1E = ({params}: LoaderFunctionArgs) => getJson<KarakterInputs & { isPublic: boolean }>(`/api/Character1E/${params.id}`)
|
|
||||||
|
|
||||||
const loadCharacter2E = async ({params}: LoaderFunctionArgs) => {
|
|
||||||
try {
|
|
||||||
const loaded2Echaracter = await getJson<Karakter2E & { isPublic: boolean }>(`/api/Character2E/${params.id}`)
|
|
||||||
store.dispatch(setCharacter(loaded2Echaracter))
|
|
||||||
return loaded2Echaracter;
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
isPublic: false,
|
|
||||||
error: error instanceof Error ? error.message : "Failed to load character",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
element: <RootLayout />,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
index: true,
|
|
||||||
element: <IndexRedirect />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "karaktereim",
|
|
||||||
element: <CharacterList />,
|
|
||||||
loader: loadCharacterList,
|
|
||||||
ErrorBoundary: ErrorBoundary,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "1e",
|
|
||||||
loader: () => redirect("/1e/karakter")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "1e/karakter",
|
|
||||||
element: <CreateCharacter1ERoute />,
|
|
||||||
ErrorBoundary: ErrorBoundary,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "1e/karakter/:id",
|
|
||||||
element: <CreateCharacter1ERoute />,
|
|
||||||
loader: loadCharacter1E,
|
|
||||||
ErrorBoundary: ErrorBoundary,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "2e",
|
|
||||||
loader: () => redirect("/2e/karakter")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "2e/karakter",
|
|
||||||
element: <CreateCharacter2ERoute />,
|
|
||||||
ErrorBoundary: ErrorBoundary,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "2e/karakter/:id",
|
|
||||||
element: <CreateCharacter2ERoute />,
|
|
||||||
loader: loadCharacter2E,
|
|
||||||
ErrorBoundary: ErrorBoundary,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
function App(props: { faro?: Faro }) {
|
function App(props: { faro?: Faro }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<FaroContext.Provider value={props.faro}>
|
<Router faro={props.faro} />
|
||||||
<UserBootstrap />
|
|
||||||
<RouterProvider router={router}/>
|
|
||||||
</FaroContext.Provider>
|
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,32 @@
|
|||||||
import {KarakterInputs} from "../domain-models/karakter";
|
import {KarakterInputs} from "../domain-models/karakter";
|
||||||
import {postJson} from "../../shared/api/http";
|
import axios from "axios";
|
||||||
|
|
||||||
export async function StoreNewCharacter(karakter: KarakterInputs, isPublic: boolean = false) {
|
export async function StoreNewCharacter(karakter: KarakterInputs, isPublic: boolean = false) {
|
||||||
return postJson<KarakterInputs, string>("/api/Character1E/", karakter, {
|
let response = await axios.post(`${window.location.origin}/api/Character1E/`, karakter, {
|
||||||
|
withCredentials: true,
|
||||||
params: {
|
params: {
|
||||||
isPublic,
|
isPublic,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if (response.status < 300){
|
||||||
|
return response.data as string
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw Error(response.statusText)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function UpdateCharacter(id: string, karakter: KarakterInputs, isPublic: boolean = false) {
|
export async function UpdateCharacter(id: string, karakter: KarakterInputs, isPublic: boolean = false) {
|
||||||
return postJson<KarakterInputs, string>(`/api/Character1E/${id}`, karakter, {
|
let response = await axios.post(`${window.location.origin}/api/Character1E/${id}`, karakter, {
|
||||||
|
withCredentials: true,
|
||||||
params: {
|
params: {
|
||||||
isPublic,
|
isPublic,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if (response.status < 300){
|
||||||
|
return response.data as string
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw Error(response.statusText)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,32 @@
|
|||||||
import {postJson} from "../../shared/api/http";
|
import axios from "axios";
|
||||||
import {Karakter2E} from "../domain-models/karakter2E";
|
import {Karakter2E} from "../domain-models/karakter2E";
|
||||||
|
|
||||||
export async function StoreNewCharacter2E(karakter: Karakter2E, isPublic: boolean = false) {
|
export async function StoreNewCharacter2E(karakter: Karakter2E, isPublic: boolean = false) {
|
||||||
return postJson<Karakter2E, string>("/api/Character2E/", karakter, {
|
let response = await axios.post(`${window.location.origin}/api/Character2E/`, karakter, {
|
||||||
|
withCredentials: true,
|
||||||
params: {
|
params: {
|
||||||
isPublic,
|
isPublic,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if (response.status < 300){
|
||||||
|
return response.data as string
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw Error(response.statusText)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function UpdateCharacter2E(id: string, karakter: Karakter2E, isPublic: boolean = false) {
|
export async function UpdateCharacter2E(id: string, karakter: Karakter2E, isPublic: boolean = false) {
|
||||||
return postJson<Karakter2E, string>(`/api/Character2E/${id}`, karakter, {
|
let response = await axios.post(`${window.location.origin}/api/Character2E/${id}`, karakter, {
|
||||||
|
withCredentials: true,
|
||||||
params: {
|
params: {
|
||||||
isPublic,
|
isPublic,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if (response.status < 300){
|
||||||
|
return response.data as string
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw Error(response.statusText)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,23 +8,21 @@ import {AppDispatch, RootState} from "../../store";
|
|||||||
import {characterSelector, setHarcosSpecialization} from "../domain-models/characterSlice";
|
import {characterSelector, setHarcosSpecialization} from "../domain-models/characterSlice";
|
||||||
|
|
||||||
function HarcosFegyverSpecializacio(props: {
|
function HarcosFegyverSpecializacio(props: {
|
||||||
harcosSzint: number,
|
szint: number
|
||||||
karakterSzint: number,
|
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
harcosSzint,
|
szint
|
||||||
karakterSzint,
|
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const dispatch = useDispatch.withTypes<AppDispatch>()()
|
const dispatch = useDispatch.withTypes<AppDispatch>()()
|
||||||
const character = useSelector.withTypes<RootState>()(characterSelector)
|
const character = useSelector.withTypes<RootState>()(characterSelector)
|
||||||
const specialization = character.szintlepesek[karakterSzint-1]?.harcosFegyver || 'kard_hosszu'
|
const specialization = character.szintlepesek[szint-1].harcosFegyver || 'kard_hosszu'
|
||||||
const existingSpecializations = character.szintlepesek.filter(x => x.harcosFegyver != null).map(x => x.harcosFegyver! as string)
|
const existingSpecializations = character.szintlepesek.filter(x => x.harcosFegyver != null).map(x => x.harcosFegyver! as string)
|
||||||
const changeSpecialization = (specialization: string) => dispatch(setHarcosSpecialization({szint: karakterSzint, fegyver: specialization}))
|
const changeSpecialization = (specialization: string) => dispatch(setHarcosSpecialization({szint, fegyver: specialization}))
|
||||||
|
|
||||||
const fegyver = GetFegyver(specialization)
|
const fegyver = GetFegyver(specialization)
|
||||||
const availableFegyverek = AllowedFegyver(Osztaly2E.Harcos)
|
const availableFegyverek = AllowedFegyver(Osztaly2E.Harcos)
|
||||||
const pickableFegyverek = availableFegyverek.filter(f => f.Id === specialization || existingSpecializations.filter(x => x === f.Id).length < (harcosSzint < 9 ? 1 : 2))
|
const pickableFegyverek = availableFegyverek.filter(f => f.Id === specialization || existingSpecializations.filter(x => x === f.Id).length < (szint < 9 ? 1 : 2))
|
||||||
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
@@ -34,7 +32,7 @@ function HarcosFegyverSpecializacio(props: {
|
|||||||
fegyverek={pickableFegyverek}
|
fegyverek={pickableFegyverek}
|
||||||
selectedId={specialization}
|
selectedId={specialization}
|
||||||
onChange={changeSpecialization}
|
onChange={changeSpecialization}
|
||||||
dataTestId={"harcos-specialization-" + karakterSzint}
|
dataTestId={"harcos-specialization-" + szint}
|
||||||
/>
|
/>
|
||||||
{fegyver == null && <span className='form-field-error'>Ismeretlen fegyver: {specialization}</span>}
|
{fegyver == null && <span className='form-field-error'>Ismeretlen fegyver: {specialization}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import {render} from "@testing-library/react";
|
|
||||||
import {configureStore} from "@reduxjs/toolkit";
|
|
||||||
import {Provider} from "react-redux";
|
|
||||||
import KarakterKepzettsegek from "./Kepzettsegek2E";
|
|
||||||
import userReducer from "../../shared/domain-models/userSlice";
|
|
||||||
import characterReducer, {setCharacter} from "../domain-models/characterSlice";
|
|
||||||
import {DefaultKarakter, Karakter2E} from "../domain-models/karakter2E";
|
|
||||||
import {Osztaly2E} from "../domain-models/osztaly2E";
|
|
||||||
import {Tulajdonsag2E} from "../domain-models/tulajdonsag2E";
|
|
||||||
|
|
||||||
function createStore() {
|
|
||||||
return configureStore({
|
|
||||||
reducer: {
|
|
||||||
user: userReducer,
|
|
||||||
character2E: characterReducer,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("KarakterKepzettsegek", () => {
|
|
||||||
it("shows one more kepzettseg selector when INT modifier increases from level-up", () => {
|
|
||||||
const karakter: Karakter2E = {
|
|
||||||
...DefaultKarakter,
|
|
||||||
szint: 4,
|
|
||||||
tulajdonsagok: {
|
|
||||||
...DefaultKarakter.tulajdonsagok,
|
|
||||||
t_int: 17,
|
|
||||||
},
|
|
||||||
szintlepesek: [
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 10 },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 7 },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 6 },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 8, tulajdonsagNoveles: Tulajdonsag2E.Intelligencia },
|
|
||||||
],
|
|
||||||
tolvajKepzettsegek: [],
|
|
||||||
varazslatok: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const store = createStore();
|
|
||||||
store.dispatch(setCharacter(karakter));
|
|
||||||
|
|
||||||
const sut = render(
|
|
||||||
<Provider store={store}>
|
|
||||||
<KarakterKepzettsegek />
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectors = sut.container.querySelectorAll('[data-testid^="kepzettseg-"]');
|
|
||||||
expect(selectors).toHaveLength(7);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows one more kepzettseg selector when INT modifier increases at level 8", () => {
|
|
||||||
const karakter: Karakter2E = {
|
|
||||||
...DefaultKarakter,
|
|
||||||
szint: 8,
|
|
||||||
tulajdonsagok: {
|
|
||||||
...DefaultKarakter.tulajdonsagok,
|
|
||||||
t_int: 14,
|
|
||||||
},
|
|
||||||
szintlepesek: [
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 10 },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 7 },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 6 },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 8, tulajdonsagNoveles: Tulajdonsag2E.Ero },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 4 },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 9 },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 5 },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 6, tulajdonsagNoveles: Tulajdonsag2E.Intelligencia },
|
|
||||||
],
|
|
||||||
tolvajKepzettsegek: [],
|
|
||||||
varazslatok: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const store = createStore();
|
|
||||||
store.dispatch(setCharacter(karakter));
|
|
||||||
|
|
||||||
const sut = render(
|
|
||||||
<Provider store={store}>
|
|
||||||
<KarakterKepzettsegek />
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectors = sut.container.querySelectorAll('[data-testid^="kepzettseg-"]');
|
|
||||||
expect(selectors).toHaveLength(6);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows one more kepzettseg selector when INT modifier increases at level 12", () => {
|
|
||||||
const karakter: Karakter2E = {
|
|
||||||
...DefaultKarakter,
|
|
||||||
szint: 12,
|
|
||||||
tulajdonsagok: {
|
|
||||||
...DefaultKarakter.tulajdonsagok,
|
|
||||||
t_int: 17,
|
|
||||||
},
|
|
||||||
szintlepesek: [
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 10 },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 7 },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 6 },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 8, tulajdonsagNoveles: Tulajdonsag2E.Ero },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 4 },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 9 },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 5 },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 6, tulajdonsagNoveles: Tulajdonsag2E.Egeszseg },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 3 },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 7 },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 5 },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 4, tulajdonsagNoveles: Tulajdonsag2E.Intelligencia },
|
|
||||||
],
|
|
||||||
tolvajKepzettsegek: [],
|
|
||||||
varazslatok: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const store = createStore();
|
|
||||||
store.dispatch(setCharacter(karakter));
|
|
||||||
|
|
||||||
const sut = render(
|
|
||||||
<Provider store={store}>
|
|
||||||
<KarakterKepzettsegek />
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectors = sut.container.querySelectorAll('[data-testid^="kepzettseg-"]');
|
|
||||||
expect(selectors).toHaveLength(7);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -13,7 +13,7 @@ import {arraySetN} from "../../util";
|
|||||||
import {useDispatch, useSelector} from "react-redux";
|
import {useDispatch, useSelector} from "react-redux";
|
||||||
import {AppDispatch, RootState} from "../../store";
|
import {AppDispatch, RootState} from "../../store";
|
||||||
import {characterSelector, setKepzettsegek, setTolvajKepzettsegek} from "../domain-models/characterSlice";
|
import {characterSelector, setKepzettsegek, setTolvajKepzettsegek} from "../domain-models/characterSlice";
|
||||||
import {TulajdonsagokTotal} from "../domain-models/tulajdonsag2E";
|
import {TulajdonsagokFajjal} from "../domain-models/faj2E";
|
||||||
|
|
||||||
function InternalKepzettsegekSelector(props: {
|
function InternalKepzettsegekSelector(props: {
|
||||||
title: string,
|
title: string,
|
||||||
@@ -70,8 +70,8 @@ function KarakterKepzettsegek () {
|
|||||||
const karakter = useSelector.withTypes<RootState>()(characterSelector);
|
const karakter = useSelector.withTypes<RootState>()(characterSelector);
|
||||||
const dispatch = useDispatch.withTypes<AppDispatch>()();
|
const dispatch = useDispatch.withTypes<AppDispatch>()();
|
||||||
|
|
||||||
const tulajdonsagokTotal = TulajdonsagokTotal(karakter)
|
const tulajdonsagokFajjal = TulajdonsagokFajjal(karakter.tulajdonsagok, karakter.faj)
|
||||||
const numberOfKepzettsegek = GetNumberOfKepzettsegek(tulajdonsagokTotal.t_int, karakter.faj)
|
const numberOfKepzettsegek = GetNumberOfKepzettsegek(tulajdonsagokFajjal.t_int, karakter.faj)
|
||||||
const kepzettsegek = karakter.kepzettsegek
|
const kepzettsegek = karakter.kepzettsegek
|
||||||
const fajError = karakter.kepzettsegFajError
|
const fajError = karakter.kepzettsegFajError
|
||||||
const osztalyError = karakter.kepzettsegOsztalyError
|
const osztalyError = karakter.kepzettsegOsztalyError
|
||||||
|
|||||||
@@ -1,248 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import {fireEvent, render} from "@testing-library/react";
|
|
||||||
import {configureStore} from "@reduxjs/toolkit";
|
|
||||||
import {Provider} from "react-redux";
|
|
||||||
import LevelUps from "./LevelUps";
|
|
||||||
import userReducer from "../../shared/domain-models/userSlice";
|
|
||||||
import characterReducer, {setCharacter} from "../domain-models/characterSlice";
|
|
||||||
import {AllowedFegyver} from "../domain-models/allowed-fegyver";
|
|
||||||
import {Osztaly2E} from "../domain-models/osztaly2E";
|
|
||||||
import {ChangeOsztalyAtSzint, DefaultKarakter, Karakter2E} from "../domain-models/karakter2E";
|
|
||||||
import {LevelUp} from "../domain-models/szintlepes";
|
|
||||||
import {GetAvailableKepzettsegek} from "../domain-models/kepzettsegek2E";
|
|
||||||
|
|
||||||
function applyLevelUp(karakter: Karakter2E): Karakter2E {
|
|
||||||
let next: Karakter2E | undefined;
|
|
||||||
LevelUp(karakter, updated => {
|
|
||||||
next = updated;
|
|
||||||
});
|
|
||||||
expect(next).toBeDefined();
|
|
||||||
return next as Karakter2E;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("LevelUps Harcos specialization indexing", () => {
|
|
||||||
it("updates specialization on the selected character level for multiclass", async () => {
|
|
||||||
const level1Tolvaj: Karakter2E = {
|
|
||||||
...DefaultKarakter,
|
|
||||||
szint: 1,
|
|
||||||
szintlepesek: [{
|
|
||||||
osztaly: Osztaly2E.Tolvaj,
|
|
||||||
HProll: 6,
|
|
||||||
}],
|
|
||||||
tolvajKepzettsegek: [],
|
|
||||||
varazslatok: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const level2Auto = applyLevelUp(level1Tolvaj);
|
|
||||||
const karakter = ChangeOsztalyAtSzint(level2Auto, Osztaly2E.Harcos, 2);
|
|
||||||
|
|
||||||
const store = configureStore({
|
|
||||||
reducer: {
|
|
||||||
user: userReducer,
|
|
||||||
character2E: characterReducer,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
store.dispatch(setCharacter(karakter));
|
|
||||||
|
|
||||||
const sut = render(
|
|
||||||
<Provider store={store}>
|
|
||||||
<LevelUps
|
|
||||||
karakter={store.getState().character2E}
|
|
||||||
changeKarakter={(updated) => store.dispatch(setCharacter(updated))}
|
|
||||||
/>
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
const selector = await sut.findByTestId("harcos-specialization-2");
|
|
||||||
|
|
||||||
const currentSpec = karakter.szintlepesek[1]?.harcosFegyver;
|
|
||||||
const nextSpec = AllowedFegyver(Osztaly2E.Harcos)
|
|
||||||
.map(x => x.Id)
|
|
||||||
.find(id => id !== currentSpec);
|
|
||||||
|
|
||||||
expect(nextSpec).toBeDefined();
|
|
||||||
fireEvent.change(selector, {target: {value: nextSpec as string}});
|
|
||||||
|
|
||||||
const stateAfter = store.getState().character2E;
|
|
||||||
expect(stateAfter.szintlepesek[0]?.harcosFegyver).toBeUndefined();
|
|
||||||
expect(stateAfter.szintlepesek[1]?.harcosFegyver).toBe(nextSpec);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not render tulajdonsag selector at level 4 when all abilities are maxed", () => {
|
|
||||||
const maxedCharacter: Karakter2E = {
|
|
||||||
...DefaultKarakter,
|
|
||||||
szint: 1,
|
|
||||||
tulajdonsagok: {
|
|
||||||
t_ero: 18,
|
|
||||||
t_ugy: 18,
|
|
||||||
t_egs: 18,
|
|
||||||
t_int: 18,
|
|
||||||
t_bol: 18,
|
|
||||||
t_kar: 18,
|
|
||||||
},
|
|
||||||
szintlepesek: [{
|
|
||||||
osztaly: Osztaly2E.Pap,
|
|
||||||
HProll: 8,
|
|
||||||
}],
|
|
||||||
tolvajKepzettsegek: [],
|
|
||||||
varazslatok: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const level2 = applyLevelUp(maxedCharacter);
|
|
||||||
const level3 = applyLevelUp(level2);
|
|
||||||
const level4 = applyLevelUp(level3);
|
|
||||||
|
|
||||||
expect(level4.szintlepesek[3]?.tulajdonsagNoveles).toBeUndefined();
|
|
||||||
|
|
||||||
const store = configureStore({
|
|
||||||
reducer: {
|
|
||||||
user: userReducer,
|
|
||||||
character2E: characterReducer,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
store.dispatch(setCharacter(level4));
|
|
||||||
|
|
||||||
const sut = render(
|
|
||||||
<Provider store={store}>
|
|
||||||
<LevelUps
|
|
||||||
karakter={store.getState().character2E}
|
|
||||||
changeKarakter={(updated) => store.dispatch(setCharacter(updated))}
|
|
||||||
/>
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(sut.queryByTestId("tulajdonsag-4")).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not render kaloz krit selector when tengeresz milestone exists but krit is missing", () => {
|
|
||||||
const inconsistentTengeresz: Karakter2E = {
|
|
||||||
...DefaultKarakter,
|
|
||||||
szint: 3,
|
|
||||||
szintlepesek: [
|
|
||||||
{
|
|
||||||
osztaly: Osztaly2E.Tengeresz,
|
|
||||||
HProll: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
osztaly: Osztaly2E.Tengeresz,
|
|
||||||
HProll: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
osztaly: Osztaly2E.Tengeresz,
|
|
||||||
HProll: 7,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
tolvajKepzettsegek: [],
|
|
||||||
varazslatok: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const store = configureStore({
|
|
||||||
reducer: {
|
|
||||||
user: userReducer,
|
|
||||||
character2E: characterReducer,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
store.dispatch(setCharacter(inconsistentTengeresz));
|
|
||||||
|
|
||||||
const sut = render(
|
|
||||||
<Provider store={store}>
|
|
||||||
<LevelUps
|
|
||||||
karakter={store.getState().character2E}
|
|
||||||
changeKarakter={(updated) => store.dispatch(setCharacter(updated))}
|
|
||||||
/>
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(sut.queryByTestId("kaloz-krit-3")).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows and updates Tolvaj extra skill on multiclass Tolvaj level 5", async () => {
|
|
||||||
const multiclassTolvaj: Karakter2E = {
|
|
||||||
...DefaultKarakter,
|
|
||||||
szint: 6,
|
|
||||||
szintlepesek: [
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 6 },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 10 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 4 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 5 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 6 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 3, tolvajExtraKepzettseg: "k_meregkeveres" },
|
|
||||||
],
|
|
||||||
tolvajKepzettsegek: ["k_alcazas", "k_csapdak", "k_egyensulyozas", "k_hamisitas"],
|
|
||||||
varazslatok: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const store = configureStore({
|
|
||||||
reducer: {
|
|
||||||
user: userReducer,
|
|
||||||
character2E: characterReducer,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
store.dispatch(setCharacter(multiclassTolvaj));
|
|
||||||
|
|
||||||
const sut = render(
|
|
||||||
<Provider store={store}>
|
|
||||||
<LevelUps
|
|
||||||
karakter={store.getState().character2E}
|
|
||||||
changeKarakter={(updated) => store.dispatch(setCharacter(updated))}
|
|
||||||
/>
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
const selector = await sut.findByTestId("tolvaj-extra-kepzettseg-6");
|
|
||||||
const nextKepzettseg = GetAvailableKepzettsegek(multiclassTolvaj)
|
|
||||||
.map(x => x.Id)
|
|
||||||
.find(id => id !== multiclassTolvaj.szintlepesek[5]?.tolvajExtraKepzettseg);
|
|
||||||
|
|
||||||
expect(nextKepzettseg).toBeDefined();
|
|
||||||
fireEvent.change(selector, {target: {value: nextKepzettseg as string}});
|
|
||||||
|
|
||||||
const stateAfter = store.getState().character2E;
|
|
||||||
expect(stateAfter.szintlepesek[5]?.tolvajExtraKepzettseg).toBe(nextKepzettseg);
|
|
||||||
expect(stateAfter.szintlepesek[4]?.tolvajExtraKepzettseg).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("at Tolvaj level 9 selector includes class-restricted non-Tolvaj skills", async () => {
|
|
||||||
const tolvajLevel9: Karakter2E = {
|
|
||||||
...DefaultKarakter,
|
|
||||||
szint: 9,
|
|
||||||
szintlepesek: [
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 6 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 5 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 4 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 3 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 2, tolvajExtraKepzettseg: "k_meregkeveres" },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 4 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 5 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 3 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 6, tolvajExtraKepzettseg: "k_alkimia" },
|
|
||||||
],
|
|
||||||
tolvajKepzettsegek: ["k_alcazas", "k_csapdak", "k_egyensulyozas", "k_hamisitas"],
|
|
||||||
varazslatok: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const store = configureStore({
|
|
||||||
reducer: {
|
|
||||||
user: userReducer,
|
|
||||||
character2E: characterReducer,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
store.dispatch(setCharacter(tolvajLevel9));
|
|
||||||
|
|
||||||
const sut = render(
|
|
||||||
<Provider store={store}>
|
|
||||||
<LevelUps
|
|
||||||
karakter={store.getState().character2E}
|
|
||||||
changeKarakter={(updated) => store.dispatch(setCharacter(updated))}
|
|
||||||
/>
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
const selector = await sut.findByTestId("tolvaj-extra-kepzettseg-9");
|
|
||||||
const arcaneOption = selector.querySelector('option[value="k_csillagjoslas"]');
|
|
||||||
expect(arcaneOption).not.toBeNull();
|
|
||||||
|
|
||||||
fireEvent.change(selector, {target: {value: "k_csillagjoslas"}});
|
|
||||||
expect(store.getState().character2E.szintlepesek[8]?.tolvajExtraKepzettseg).toBe("k_csillagjoslas");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -41,8 +41,6 @@ function LevelUp(props: {
|
|||||||
const kalozSzint = classLevels[Osztaly2E.Tengeresz]
|
const kalozSzint = classLevels[Osztaly2E.Tengeresz]
|
||||||
const tolvajSzint = classLevels[Osztaly2E.Tolvaj]
|
const tolvajSzint = classLevels[Osztaly2E.Tolvaj]
|
||||||
const szintlepes = karakter.szintlepesek[szint-1];
|
const szintlepes = karakter.szintlepesek[szint-1];
|
||||||
const tulajdonsagNoveles = szintlepes?.tulajdonsagNoveles;
|
|
||||||
const kalozKritikus = szintlepes?.kalozKritikus;
|
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<div className='row mt-3'>
|
<div className='row mt-3'>
|
||||||
@@ -56,10 +54,10 @@ function LevelUp(props: {
|
|||||||
osztalySzint={classLevels[osztaly]}
|
osztalySzint={classLevels[osztaly]}
|
||||||
dataTestId={`osztaly-${szint}`}
|
dataTestId={`osztaly-${szint}`}
|
||||||
/>}
|
/>}
|
||||||
{szint % 4 === 0 && tulajdonsagNoveles != null &&
|
{szint % 4 === 0 &&
|
||||||
<TulajdonsagNoveles
|
<TulajdonsagNoveles
|
||||||
tulajdonsagok={karakter.tulajdonsagok}
|
tulajdonsagok={karakter.tulajdonsagok}
|
||||||
tulajdonsagNoveles={tulajdonsagNoveles}
|
tulajdonsagNoveles={karakter.szintlepesek[szint-1].tulajdonsagNoveles!}
|
||||||
changeTulajdonsagNoveles={changeTulajdonsagNoveles}
|
changeTulajdonsagNoveles={changeTulajdonsagNoveles}
|
||||||
dataTestId={`tulajdonsag-${szint}`}
|
dataTestId={`tulajdonsag-${szint}`}
|
||||||
/>}
|
/>}
|
||||||
@@ -71,13 +69,12 @@ function LevelUp(props: {
|
|||||||
/>
|
/>
|
||||||
{osztaly === Osztaly2E.Harcos && harcosSzint % 2 === 1 &&
|
{osztaly === Osztaly2E.Harcos && harcosSzint % 2 === 1 &&
|
||||||
<HarcosFegyverSpecializacio
|
<HarcosFegyverSpecializacio
|
||||||
harcosSzint={harcosSzint}
|
szint={harcosSzint}
|
||||||
karakterSzint={szint}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{osztaly === Osztaly2E.Tengeresz && kalozSzint % 3 === 0 && kalozKritikus != null &&
|
{osztaly === Osztaly2E.Tengeresz && kalozSzint % 3 === 0 &&
|
||||||
<KalozKritikus
|
<KalozKritikus
|
||||||
kritFegyverId={kalozKritikus}
|
kritFegyverId={szintlepes.kalozKritikus!}
|
||||||
existingKrits={karakter.szintlepesek.map(x => x.kalozKritikus)}
|
existingKrits={karakter.szintlepesek.map(x => x.kalozKritikus)}
|
||||||
changeKrit={changeKalozKritikus}
|
changeKrit={changeKalozKritikus}
|
||||||
szint={kalozSzint}
|
szint={kalozSzint}
|
||||||
@@ -85,17 +82,12 @@ function LevelUp(props: {
|
|||||||
}
|
}
|
||||||
{osztaly === Osztaly2E.Tolvaj
|
{osztaly === Osztaly2E.Tolvaj
|
||||||
&& (tolvajSzint === 5 || tolvajSzint === 9)
|
&& (tolvajSzint === 5 || tolvajSzint === 9)
|
||||||
&& szintlepes?.tolvajExtraKepzettseg != null &&
|
&& szintlepes.tolvajExtraKepzettseg != null &&
|
||||||
<div className='row m-2'>
|
<div className='row m-2'>
|
||||||
<label className='col-md-2 col-sm-3 col-form-label'>Extra képzettség</label>
|
<label className='col-md-2 col-sm-3 col-form-label'>Extra képzettség</label>
|
||||||
<KepzettsegSelector
|
<KepzettsegSelector
|
||||||
selected={Kepzettsegek[szintlepes!.tolvajExtraKepzettseg]}
|
selected={Kepzettsegek[szintlepes.tolvajExtraKepzettseg]}
|
||||||
kepzettsegek={[
|
kepzettsegek={[Kepzettsegek[szintlepes.tolvajExtraKepzettseg], ...GetAvailableKepzettsegek(karakter)]}
|
||||||
Kepzettsegek[szintlepes!.tolvajExtraKepzettseg],
|
|
||||||
...GetAvailableKepzettsegek(karakter, {
|
|
||||||
ignoreClassRestrictions: tolvajSzint === 9,
|
|
||||||
})
|
|
||||||
]}
|
|
||||||
dataTestId={`tolvaj-extra-kepzettseg-${szint}`}
|
dataTestId={`tolvaj-extra-kepzettseg-${szint}`}
|
||||||
changeKepzettseg={changeExtraTolvajKepzettseg}
|
changeKepzettseg={changeExtraTolvajKepzettseg}
|
||||||
/>
|
/>
|
||||||
@@ -108,19 +100,16 @@ export function LevelUps(props: {karakter: Karakter2E, changeKarakter: (inputs:
|
|||||||
const {karakter, changeKarakter} = props;
|
const {karakter, changeKarakter} = props;
|
||||||
|
|
||||||
const tulajdonsagokAtSzint = (szint: number) => TulajdonsagokTotal({...karakter, szintlepesek: karakter.szintlepesek.slice(0, szint)})
|
const tulajdonsagokAtSzint = (szint: number) => TulajdonsagokTotal({...karakter, szintlepesek: karakter.szintlepesek.slice(0, szint)})
|
||||||
const szintlepesAt = (szint: number) => karakter.szintlepesek[szint-1]!
|
|
||||||
|
|
||||||
const changeRolledHPAtSzint = (szint: number) => (newHProll: number) => {
|
const changeRolledHPAtSzint = (szint: number) => (newHProll: number) => {
|
||||||
const szintlepes = szintlepesAt(szint)
|
changeKarakter({...karakter, szintlepesek: arraySetN(karakter.szintlepesek, szint-1, {...karakter.szintlepesek[szint-1], HProll: newHProll})})
|
||||||
changeKarakter({...karakter, szintlepesek: arraySetN(karakter.szintlepesek, szint-1, {...szintlepes, HProll: newHProll})})
|
|
||||||
console.log(karakter)
|
console.log(karakter)
|
||||||
}
|
}
|
||||||
const changeTulajdonsagNovelesAtSzint = (szint: number) => {
|
const changeTulajdonsagNovelesAtSzint = (szint: number) => {
|
||||||
return (t: Tulajdonsag2E) => {
|
return (t: Tulajdonsag2E) => {
|
||||||
const szintlepes = szintlepesAt(szint)
|
|
||||||
changeKarakter({...karakter,
|
changeKarakter({...karakter,
|
||||||
szintlepesek: arraySetN(karakter.szintlepesek, szint-1, {
|
szintlepesek: arraySetN(karakter.szintlepesek, szint-1, {
|
||||||
...szintlepes,
|
...karakter.szintlepesek[szint-1],
|
||||||
tulajdonsagNoveles: t
|
tulajdonsagNoveles: t
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -129,11 +118,10 @@ export function LevelUps(props: {karakter: Karakter2E, changeKarakter: (inputs:
|
|||||||
|
|
||||||
const changeKalozKritikusAtSzint = (szint: number) => {
|
const changeKalozKritikusAtSzint = (szint: number) => {
|
||||||
return (krit: string) => {
|
return (krit: string) => {
|
||||||
const szintlepes = szintlepesAt(szint)
|
|
||||||
changeKarakter({
|
changeKarakter({
|
||||||
...karakter,
|
...karakter,
|
||||||
szintlepesek: arraySetN(karakter.szintlepesek, szint-1, {
|
szintlepesek: arraySetN(karakter.szintlepesek, szint-1, {
|
||||||
...szintlepes,
|
...karakter.szintlepesek[szint-1],
|
||||||
kalozKritikus: krit
|
kalozKritikus: krit
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -142,11 +130,10 @@ export function LevelUps(props: {karakter: Karakter2E, changeKarakter: (inputs:
|
|||||||
|
|
||||||
const changeExtraTolvajKepzettseg = (szint: number) => {
|
const changeExtraTolvajKepzettseg = (szint: number) => {
|
||||||
return (k: KepzettsegId) => {
|
return (k: KepzettsegId) => {
|
||||||
const szintlepes = szintlepesAt(szint)
|
|
||||||
changeKarakter({
|
changeKarakter({
|
||||||
...karakter,
|
...karakter,
|
||||||
szintlepesek: arraySetN(karakter.szintlepesek, szint-1, {
|
szintlepesek: arraySetN(karakter.szintlepesek, szint-1, {
|
||||||
...szintlepes,
|
...karakter.szintlepesek[szint-1],
|
||||||
tolvajExtraKepzettseg: k,
|
tolvajExtraKepzettseg: k,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -157,13 +144,10 @@ export function LevelUps(props: {karakter: Karakter2E, changeKarakter: (inputs:
|
|||||||
|
|
||||||
return <>
|
return <>
|
||||||
{levels.slice(1).map(szint =>
|
{levels.slice(1).map(szint =>
|
||||||
{
|
|
||||||
const szintlepes = szintlepesAt(szint)
|
|
||||||
return (
|
|
||||||
<LevelUp
|
<LevelUp
|
||||||
key={`level-up-${szint}`}
|
key={`level-up-${szint}`}
|
||||||
szint={szint}
|
szint={szint}
|
||||||
osztaly={szintlepes.osztaly}
|
osztaly={karakter.szintlepesek[szint-1].osztaly}
|
||||||
karakter={{...karakter, szintlepesek: karakter.szintlepesek.slice(0, szint) ,tulajdonsagok: tulajdonsagokAtSzint(szint)}}
|
karakter={{...karakter, szintlepesek: karakter.szintlepesek.slice(0, szint) ,tulajdonsagok: tulajdonsagokAtSzint(szint)}}
|
||||||
changeTulajdonsagNoveles={changeTulajdonsagNovelesAtSzint(szint)}
|
changeTulajdonsagNoveles={changeTulajdonsagNovelesAtSzint(szint)}
|
||||||
changeRolledHP={changeRolledHPAtSzint(szint)}
|
changeRolledHP={changeRolledHPAtSzint(szint)}
|
||||||
@@ -171,8 +155,6 @@ export function LevelUps(props: {karakter: Karakter2E, changeKarakter: (inputs:
|
|||||||
changeOsztaly={(o) => changeKarakter(ChangeOsztalyAtSzint(karakter, o, szint))}
|
changeOsztaly={(o) => changeKarakter(ChangeOsztalyAtSzint(karakter, o, szint))}
|
||||||
changeExtraTolvajKepzettseg={changeExtraTolvajKepzettseg(szint)}
|
changeExtraTolvajKepzettseg={changeExtraTolvajKepzettseg(szint)}
|
||||||
/>
|
/>
|
||||||
)
|
|
||||||
}
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {AllowedFegyver} from "./allowed-fegyver";
|
|||||||
import {BaseHP} from "./masodlagos_ertekek";
|
import {BaseHP} from "./masodlagos_ertekek";
|
||||||
import {dAny} from "../../shared/domain-models/kockak";
|
import {dAny} from "../../shared/domain-models/kockak";
|
||||||
import {KarakterVarazslat} from "./varazslat";
|
import {KarakterVarazslat} from "./varazslat";
|
||||||
import {GetAvailableKepzettsegek} from "./kepzettsegek2E";
|
|
||||||
|
|
||||||
export interface Karakter2E {
|
export interface Karakter2E {
|
||||||
id?: string,
|
id?: string,
|
||||||
@@ -52,15 +51,13 @@ export const DefaultKarakter: Karakter2E = {
|
|||||||
varazslatok: [],
|
varazslatok: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOsztalyForSzintLepes(karakter: Karakter2E, osztaly: Osztaly2E, szint: number): Szintlepes[] {
|
function setOsztalyForSzintLepes(szintlepesek: Szintlepes[], osztaly: Osztaly2E, szint: number): Szintlepes[] {
|
||||||
const szintlepesek = karakter.szintlepesek
|
|
||||||
const prev = szintlepesek.slice(0, szint-1)
|
const prev = szintlepesek.slice(0, szint-1)
|
||||||
const current : Szintlepes = {
|
const current : Szintlepes = {
|
||||||
...szintlepesek[szint-1],
|
...szintlepesek[szint-1],
|
||||||
osztaly,
|
osztaly,
|
||||||
harcosFegyver: undefined,
|
harcosFegyver: undefined,
|
||||||
kalozKritikus: undefined,
|
kalozKritikus: undefined
|
||||||
tolvajExtraKepzettseg: undefined,
|
|
||||||
}
|
}
|
||||||
const tail = szintlepesek.slice(szint).map(x => {return {...x}})
|
const tail = szintlepesek.slice(szint).map(x => {return {...x}})
|
||||||
|
|
||||||
@@ -121,39 +118,6 @@ function setOsztalyForSzintLepes(karakter: Karakter2E, osztaly: Osztaly2E, szint
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let tolvajSzint = prev.filter(x => x.osztaly === Osztaly2E.Tolvaj).length
|
|
||||||
const existingTolvajExtraKepzettsegek = szintlepesek
|
|
||||||
.map(x => x.tolvajExtraKepzettseg)
|
|
||||||
.filter(x => x != null) as KepzettsegId[]
|
|
||||||
const tolvajExtraKepzettsegAtSzint = (tSzint: number, atLevelSzintlepesek: Szintlepes[]): (KepzettsegId | undefined) => {
|
|
||||||
if (tSzint !== 5 && tSzint !== 9) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
const extraIndex = tSzint === 5 ? 0 : 1
|
|
||||||
if (existingTolvajExtraKepzettsegek.length > extraIndex) {
|
|
||||||
return existingTolvajExtraKepzettsegek[extraIndex]
|
|
||||||
}
|
|
||||||
return GetAvailableKepzettsegek({
|
|
||||||
szintlepesek: atLevelSzintlepesek,
|
|
||||||
faj: karakter.faj,
|
|
||||||
kepzettsegek: karakter.kepzettsegek,
|
|
||||||
tolvajKepzettsegek: karakter.tolvajKepzettsegek,
|
|
||||||
}, {
|
|
||||||
ignoreClassRestrictions: tSzint === 9,
|
|
||||||
})[0]?.Id
|
|
||||||
}
|
|
||||||
|
|
||||||
if (osztaly === Osztaly2E.Tolvaj) {
|
|
||||||
tolvajSzint++
|
|
||||||
current.tolvajExtraKepzettseg = tolvajExtraKepzettsegAtSzint(tolvajSzint, [...prev, current])
|
|
||||||
}
|
|
||||||
for (let i = 0; i < tail.length; i++) {
|
|
||||||
if (tail[i].osztaly === Osztaly2E.Tolvaj) {
|
|
||||||
tolvajSzint++
|
|
||||||
tail[i].tolvajExtraKepzettseg = tolvajExtraKepzettsegAtSzint(tolvajSzint, [...prev, current, ...tail.slice(0, i+1)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const HD = BaseHP(osztaly)
|
const HD = BaseHP(osztaly)
|
||||||
if (szint === 1) {
|
if (szint === 1) {
|
||||||
current.HProll = HD
|
current.HProll = HD
|
||||||
@@ -174,6 +138,6 @@ export function ChangeLvl1Osztaly(karakter: Karakter2E, osztaly: Osztaly2E): Kar
|
|||||||
export function ChangeOsztalyAtSzint(karakter: Karakter2E, osztaly: Osztaly2E, szint: number): Karakter2E {
|
export function ChangeOsztalyAtSzint(karakter: Karakter2E, osztaly: Osztaly2E, szint: number): Karakter2E {
|
||||||
return {
|
return {
|
||||||
...karakter,
|
...karakter,
|
||||||
szintlepesek: setOsztalyForSzintLepes(karakter, osztaly, szint)
|
szintlepesek: setOsztalyForSzintLepes(karakter.szintlepesek, osztaly, szint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,28 +106,6 @@ describe("Kepzettsegek 2E", () => {
|
|||||||
const actual = GetAvailableKepzettsegek(karakter)
|
const actual = GetAvailableKepzettsegek(karakter)
|
||||||
expect(actual).not.toContain(Kepzettsegek.k_meregkeveres)
|
expect(actual).not.toContain(Kepzettsegek.k_meregkeveres)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("at Tolvaj level 9 extra skill can include class-restricted non-Tolvaj skills", () => {
|
|
||||||
const karakter = {
|
|
||||||
...DefaultKarakter,
|
|
||||||
szintlepesek: [
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 6, },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 6, },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 6, },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 6, tulajdonsagNoveles: Tulajdonsag2E.Ugyesseg },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 6, tolvajExtraKepzettseg: Kepzettsegek.k_meregkeveres.Id },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 6, },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 6, },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 6, },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const restricted = GetAvailableKepzettsegek(karakter)
|
|
||||||
expect(restricted).not.toContain(Kepzettsegek.k_csillagjoslas)
|
|
||||||
|
|
||||||
const unrestricted = GetAvailableKepzettsegek(karakter, { ignoreClassRestrictions: true })
|
|
||||||
expect(unrestricted).toContain(Kepzettsegek.k_csillagjoslas)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
test("multiclass characters get access to kepzettsegek from all classes", () => {
|
test("multiclass characters get access to kepzettsegek from all classes", () => {
|
||||||
const karakter = {
|
const karakter = {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function availableKezpettsegListFajjal(faj: Faj2E, ...osztalyok: Osztaly2E[]): K
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function GetNumberOfKepzettsegek(t_int: number, faj: Faj2E, max: number = 11) {
|
export function GetNumberOfKepzettsegek(t_int: number, faj: Faj2E, max: number = 11) {
|
||||||
//max = 3 + 1 + 3 + 4 + 2 = 13 // 3 base, 1 human, 3 from max ability modifier, 4 from Thief +1 at 5th and +1 at 9th level
|
//max = 3 + 1 + 3 + 5 = 12 // 3 base, 1 human, 3 from max ability modifier, 4 from Thief +1 at 9th level
|
||||||
let numberOfKepzettseg = 3 + Modifier(t_int) + (faj === Faj2E.Ember ? 1 : 0)
|
let numberOfKepzettseg = 3 + Modifier(t_int) + (faj === Faj2E.Ember ? 1 : 0)
|
||||||
|
|
||||||
if (numberOfKepzettseg < 1) {
|
if (numberOfKepzettseg < 1) {
|
||||||
@@ -27,15 +27,9 @@ export function GetNumberOfKepzettsegek(t_int: number, faj: Faj2E, max: number =
|
|||||||
return numberOfKepzettseg;
|
return numberOfKepzettseg;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GetAvailableKepzettsegek(
|
export function GetAvailableKepzettsegek(karakter: Pick<Karakter2E, 'szintlepesek' | 'faj' | 'kepzettsegek' | 'tolvajKepzettsegek'>): Kepzettseg[] {
|
||||||
karakter: Pick<Karakter2E, 'szintlepesek' | 'faj' | 'kepzettsegek' | 'tolvajKepzettsegek'>,
|
|
||||||
options?: { ignoreClassRestrictions?: boolean }
|
|
||||||
): Kepzettseg[] {
|
|
||||||
const osztalyok = new Set(karakter.szintlepesek.map(x => x.osztaly))
|
const osztalyok = new Set(karakter.szintlepesek.map(x => x.osztaly))
|
||||||
const baseKepzettsegList = options?.ignoreClassRestrictions
|
const availableKepzettsegek = availableKezpettsegListFajjal(karakter.faj, ...osztalyok)
|
||||||
? KepzettsegLista
|
|
||||||
: availableKezpettsegListFajjal(karakter.faj, ...osztalyok)
|
|
||||||
const availableKepzettsegek = baseKepzettsegList
|
|
||||||
.filter(k => !karakter.kepzettsegek.includes(k.Id))
|
.filter(k => !karakter.kepzettsegek.includes(k.Id))
|
||||||
|
|
||||||
if (!osztalyok.has(Osztaly2E.Tolvaj)) {
|
if (!osztalyok.has(Osztaly2E.Tolvaj)) {
|
||||||
|
|||||||
@@ -1,151 +0,0 @@
|
|||||||
import {ChangeOsztalyAtSzint, DefaultKarakter, Karakter2E} from "./karakter2E";
|
|
||||||
import {Osztaly2E} from "./osztaly2E";
|
|
||||||
import {LevelUp} from "./szintlepes";
|
|
||||||
|
|
||||||
function applyLevelUp(karakter: Karakter2E): Karakter2E {
|
|
||||||
let next: Karakter2E | undefined;
|
|
||||||
LevelUp(karakter, updated => {
|
|
||||||
next = updated;
|
|
||||||
});
|
|
||||||
expect(next).toBeDefined();
|
|
||||||
return next as Karakter2E;
|
|
||||||
}
|
|
||||||
|
|
||||||
function level1Tolvaj(): Karakter2E {
|
|
||||||
return {
|
|
||||||
...DefaultKarakter,
|
|
||||||
szint: 1,
|
|
||||||
szintlepesek: [{
|
|
||||||
osztaly: Osztaly2E.Tolvaj,
|
|
||||||
HProll: 6,
|
|
||||||
}],
|
|
||||||
tolvajKepzettsegek: [],
|
|
||||||
varazslatok: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("Harcos specialization in multiclass level-ups", () => {
|
|
||||||
it("adds a specialization when first Harcos level is gained at character level 2", () => {
|
|
||||||
const afterLevel2 = applyLevelUp(level1Tolvaj());
|
|
||||||
const multiclassed = ChangeOsztalyAtSzint(afterLevel2, Osztaly2E.Harcos, 2);
|
|
||||||
|
|
||||||
const secondLevel = multiclassed.szintlepesek[1];
|
|
||||||
expect(secondLevel?.osztaly).toBe(Osztaly2E.Harcos);
|
|
||||||
expect(secondLevel?.harcosFegyver).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not add a specialization on even Harcos class level", () => {
|
|
||||||
const afterLevel2 = applyLevelUp(level1Tolvaj());
|
|
||||||
const multiclassed = ChangeOsztalyAtSzint(afterLevel2, Osztaly2E.Harcos, 2);
|
|
||||||
const afterLevel3 = applyLevelUp(multiclassed);
|
|
||||||
|
|
||||||
const thirdLevel = afterLevel3.szintlepesek[2];
|
|
||||||
expect(thirdLevel?.osztaly).toBe(Osztaly2E.Harcos);
|
|
||||||
expect(thirdLevel?.harcosFegyver).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("adds the next specialization on odd Harcos class level after multiclass", () => {
|
|
||||||
const afterLevel2 = applyLevelUp(level1Tolvaj());
|
|
||||||
const multiclassed = ChangeOsztalyAtSzint(afterLevel2, Osztaly2E.Harcos, 2);
|
|
||||||
const afterLevel3 = applyLevelUp(multiclassed);
|
|
||||||
const afterLevel4 = applyLevelUp(afterLevel3);
|
|
||||||
|
|
||||||
const firstHarcosSpec = multiclassed.szintlepesek[1]?.harcosFegyver;
|
|
||||||
const thirdHarcosLevelSpec = afterLevel4.szintlepesek[3]?.harcosFegyver;
|
|
||||||
|
|
||||||
expect(firstHarcosSpec).toBeDefined();
|
|
||||||
expect(thirdHarcosLevelSpec).toBeDefined();
|
|
||||||
expect(thirdHarcosLevelSpec).not.toBe(firstHarcosSpec);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Tolvaj extra skill in multiclass level-ups", () => {
|
|
||||||
it("adds extra skill when Tolvaj class reaches level 5 at higher character level", () => {
|
|
||||||
const karakter: Karakter2E = {
|
|
||||||
...DefaultKarakter,
|
|
||||||
szint: 5,
|
|
||||||
szintlepesek: [
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 6 },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 10 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 4 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 5 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 3 },
|
|
||||||
],
|
|
||||||
tolvajKepzettsegek: ["k_alcazas", "k_csapdak", "k_egyensulyozas", "k_hamisitas"],
|
|
||||||
varazslatok: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const afterLevel6 = applyLevelUp(karakter);
|
|
||||||
const sixthLevel = afterLevel6.szintlepesek[5];
|
|
||||||
|
|
||||||
expect(sixthLevel?.osztaly).toBe(Osztaly2E.Tolvaj);
|
|
||||||
expect(sixthLevel?.tolvajExtraKepzettseg).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("recomputes missing extra skill when class change makes current level Tolvaj level 5", () => {
|
|
||||||
const karakter: Karakter2E = {
|
|
||||||
...DefaultKarakter,
|
|
||||||
szint: 5,
|
|
||||||
szintlepesek: [
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 6 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 5 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 4 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 3 },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 8 },
|
|
||||||
],
|
|
||||||
tolvajKepzettsegek: ["k_alcazas", "k_csapdak", "k_egyensulyozas", "k_hamisitas"],
|
|
||||||
varazslatok: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const changed = ChangeOsztalyAtSzint(karakter, Osztaly2E.Tolvaj, 5);
|
|
||||||
const fifthLevel = changed.szintlepesek[4];
|
|
||||||
|
|
||||||
expect(fifthLevel?.osztaly).toBe(Osztaly2E.Tolvaj);
|
|
||||||
expect(fifthLevel?.tolvajExtraKepzettseg).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("at Tolvaj class level 9 LevelUp can auto-pick a class-restricted non-Tolvaj skill", () => {
|
|
||||||
const karakter: Karakter2E = {
|
|
||||||
...DefaultKarakter,
|
|
||||||
szint: 8,
|
|
||||||
szintlepesek: [
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 6 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 5 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 4 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 3 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 2, tolvajExtraKepzettseg: "k_meregkeveres" },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 4 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 5 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 3 },
|
|
||||||
],
|
|
||||||
tolvajKepzettsegek: ["k_alcazas", "k_csapdak", "k_egyensulyozas", "k_hamisitas"],
|
|
||||||
varazslatok: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const afterLevel9 = applyLevelUp(karakter);
|
|
||||||
expect(afterLevel9.szintlepesek[8]?.tolvajExtraKepzettseg).toBe("k_alkimia");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("at Tolvaj class level 9 class-change recomputation can set non-Tolvaj restricted skill", () => {
|
|
||||||
const karakter: Karakter2E = {
|
|
||||||
...DefaultKarakter,
|
|
||||||
szint: 9,
|
|
||||||
szintlepesek: [
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 6 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 5 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 4 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 3 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 2, tolvajExtraKepzettseg: "k_meregkeveres" },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 4 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 5 },
|
|
||||||
{ osztaly: Osztaly2E.Tolvaj, HProll: 3 },
|
|
||||||
{ osztaly: Osztaly2E.Harcos, HProll: 8 },
|
|
||||||
],
|
|
||||||
tolvajKepzettsegek: ["k_alcazas", "k_csapdak", "k_egyensulyozas", "k_hamisitas"],
|
|
||||||
varazslatok: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const changed = ChangeOsztalyAtSzint(karakter, Osztaly2E.Tolvaj, 9);
|
|
||||||
expect(changed.szintlepesek[8]?.tolvajExtraKepzettseg).toBe("k_alkimia");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -67,9 +67,7 @@ export function LevelUp(karakter: Karakter2E, changeKarakter: (input: Karakter2E
|
|||||||
if (autoPickedOsztaly === Osztaly2E.Tolvaj){
|
if (autoPickedOsztaly === Osztaly2E.Tolvaj){
|
||||||
const tolvajSzint = 1 + karakter.szintlepesek.filter(x => x.osztaly === Osztaly2E.Tolvaj).length
|
const tolvajSzint = 1 + karakter.szintlepesek.filter(x => x.osztaly === Osztaly2E.Tolvaj).length
|
||||||
if (tolvajSzint === 5 || tolvajSzint === 9) {
|
if (tolvajSzint === 5 || tolvajSzint === 9) {
|
||||||
const availableKepzettsegek = GetAvailableKepzettsegek(karakter, {
|
const availableKepzettsegek = GetAvailableKepzettsegek(karakter)
|
||||||
ignoreClassRestrictions: tolvajSzint === 9,
|
|
||||||
})
|
|
||||||
szintlepes.tolvajExtraKepzettseg = availableKepzettsegek[0].Id;
|
szintlepes.tolvajExtraKepzettseg = availableKepzettsegek[0].Id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ function CreateCharacter2E(props: {
|
|||||||
<h4 className='col align-self-center'>Tanult</h4>
|
<h4 className='col align-self-center'>Tanult</h4>
|
||||||
</div>
|
</div>
|
||||||
<OsztalySelector2E />
|
<OsztalySelector2E />
|
||||||
{karakter.szintlepesek[0].osztaly === Osztaly2E.Harcos && <HarcosFegyverSpecializacio harcosSzint={1} karakterSzint={1}/>}
|
{karakter.szintlepesek[0].osztaly === Osztaly2E.Harcos && <HarcosFegyverSpecializacio szint={1}/>}
|
||||||
<KarakterKepzettsegek />
|
<KarakterKepzettsegek />
|
||||||
<hr/>
|
<hr/>
|
||||||
{/* 1. szinten */}
|
{/* 1. szinten */}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import axios from "axios";
|
||||||
import {Container, Nav, Navbar} from "react-bootstrap";
|
import {Container, Nav, Navbar} from "react-bootstrap";
|
||||||
import {unsetUser, userSelector} from "./domain-models/userSlice";
|
import {unsetUser, userSelector} from "./domain-models/userSlice";
|
||||||
import {AppDispatch, RootState} from "../store";
|
import {AppDispatch, RootState} from "../store";
|
||||||
import {useDispatch, useSelector} from "react-redux";
|
import {useDispatch, useSelector} from "react-redux";
|
||||||
import {Link, useNavigate} from "react-router-dom";
|
import {Link, useNavigate} from "react-router-dom";
|
||||||
import {postVoid} from "./api/http";
|
|
||||||
|
|
||||||
function Header() {
|
function Header() {
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ function Header() {
|
|||||||
const fetchedUser = useSelector.withTypes<RootState>()(userSelector);
|
const fetchedUser = useSelector.withTypes<RootState>()(userSelector);
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
await postVoid("/Identity/Account/Logout")
|
await axios.post("/Identity/Account/Logout")
|
||||||
dispatch(unsetUser())
|
dispatch(unsetUser())
|
||||||
navigate("/")
|
navigate("/")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import axios, {AxiosRequestConfig} from "axios";
|
|
||||||
|
|
||||||
export const http = axios.create({
|
|
||||||
withCredentials: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function getJson<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
|
||||||
const response = await http.get<T>(url, config)
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getText(url: string, config?: AxiosRequestConfig): Promise<string> {
|
|
||||||
const response = await http.get<string>(url, {
|
|
||||||
...config,
|
|
||||||
responseType: "text",
|
|
||||||
})
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function postJson<TRequest, TResponse>(url: string, body: TRequest, config?: AxiosRequestConfig): Promise<TResponse> {
|
|
||||||
const response = await http.post<TResponse>(url, body, config)
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function postVoid<TRequest = undefined>(url: string, body?: TRequest, config?: AxiosRequestConfig): Promise<void> {
|
|
||||||
await http.post(url, body, config)
|
|
||||||
}
|
|
||||||
@@ -1,35 +1,18 @@
|
|||||||
import {createAsyncThunk, createSlice} from '@reduxjs/toolkit'
|
import { createSlice } from '@reduxjs/toolkit'
|
||||||
import {getText} from '../api/http'
|
|
||||||
|
|
||||||
type LoadingState = "not-started" | "loading" | "finished"
|
type LoadingState = "not-started" | "loading" | "finished"
|
||||||
|
|
||||||
export interface UserState {
|
|
||||||
state: LoadingState,
|
|
||||||
email: string | null,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fetchCurrentUser = createAsyncThunk<string | null, void, { state: { user: UserState } }>(
|
|
||||||
'user/fetchCurrentUser',
|
|
||||||
async () => {
|
|
||||||
try {
|
|
||||||
const userNameResponse = await getText('/api/User/me')
|
|
||||||
return userNameResponse.length > 0 ? userNameResponse : null
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
condition: (_, {getState}) => getState().user.state === 'not-started',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const userSlice = createSlice({
|
export const userSlice = createSlice({
|
||||||
name: 'user',
|
name: 'user',
|
||||||
initialState: {
|
initialState: {
|
||||||
state: "not-started" as LoadingState,
|
state: "not-started" as LoadingState,
|
||||||
email: null as string | null,
|
email: null as string | null,
|
||||||
} as UserState,
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
|
load: (state) => {
|
||||||
|
state.state = "loading"
|
||||||
|
return state
|
||||||
|
},
|
||||||
setUser: (state, action: {payload: string}) => {
|
setUser: (state, action: {payload: string}) => {
|
||||||
state.state = "finished"
|
state.state = "finished"
|
||||||
state.email = action.payload
|
state.email = action.payload
|
||||||
@@ -41,24 +24,10 @@ export const userSlice = createSlice({
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
extraReducers: builder => {
|
|
||||||
builder
|
|
||||||
.addCase(fetchCurrentUser.pending, (state) => {
|
|
||||||
state.state = 'loading'
|
|
||||||
})
|
|
||||||
.addCase(fetchCurrentUser.fulfilled, (state, action) => {
|
|
||||||
state.state = 'finished'
|
|
||||||
state.email = action.payload
|
|
||||||
})
|
|
||||||
.addCase(fetchCurrentUser.rejected, (state) => {
|
|
||||||
state.state = 'finished'
|
|
||||||
state.email = null
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Action creators are generated for each case reducer function
|
// Action creators are generated for each case reducer function
|
||||||
export const { setUser, unsetUser } = userSlice.actions
|
export const { load, setUser, unsetUser } = userSlice.actions
|
||||||
|
|
||||||
export const userSelector = userSlice.selectSlice
|
export const userSelector = userSlice.selectSlice
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ interface CharacterListItemDto2E {
|
|||||||
edition: "2e"
|
edition: "2e"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CharacterListItemDto = CharacterListItemDto1E | CharacterListItemDto2E;
|
type CharacterListItemDto = CharacterListItemDto1E | CharacterListItemDto2E;
|
||||||
|
|
||||||
function mapDtoToViewModel(dto: CharacterListItemDto): CharacterListItemViewModel {
|
function mapDtoToViewModel(dto: CharacterListItemDto): CharacterListItemViewModel {
|
||||||
if (dto.edition === "1e") {
|
if (dto.edition === "1e") {
|
||||||
|
|||||||
@@ -18,12 +18,10 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx"
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"exactOptionalPropertyTypes": true
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src",
|
"src",
|
||||||
"vite.config.mts"
|
"vite.config.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import react from '@vitejs/plugin-react';
|
|||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
const env = loadEnv(mode, process.cwd(), '');
|
const env = loadEnv(mode, process.cwd(), '');
|
||||||
const port = Number(env.PORT || 5173);
|
const port = Number(env.PORT || 5173);
|
||||||
const httpsEnabled = env.HTTPS === 'true';
|
const https = env.HTTPS === 'true';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
port,
|
port,
|
||||||
...(httpsEnabled ? { https: {} } : {}),
|
https,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
allowedHosts: ['localhost', 'host.docker.internal', 'frontend'],
|
allowedHosts: ['localhost', 'host.docker.internal', 'frontend'],
|
||||||
},
|
},
|
||||||
+31
-742
File diff suppressed because it is too large
Load Diff
@@ -1,61 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
CERT_DIR="${CERT_DIR:-/certs}"
|
|
||||||
|
|
||||||
mkdir -p "$CERT_DIR"
|
|
||||||
|
|
||||||
if [ -f "$CERT_DIR/rootCA.pem" ] && [ -f "$CERT_DIR/rootCA.key" ] && [ -f "$CERT_DIR/kemkas.pem" ] && [ -f "$CERT_DIR/kemkas.key" ]; then
|
|
||||||
echo "Using existing TLS material from $CERT_DIR"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
apk add --no-cache openssl >/dev/null
|
|
||||||
|
|
||||||
cat > /tmp/kemkas-server.cnf <<'EOF'
|
|
||||||
[req]
|
|
||||||
default_bits = 2048
|
|
||||||
prompt = no
|
|
||||||
default_md = sha256
|
|
||||||
distinguished_name = dn
|
|
||||||
req_extensions = req_ext
|
|
||||||
|
|
||||||
[dn]
|
|
||||||
CN = localhost
|
|
||||||
|
|
||||||
[req_ext]
|
|
||||||
subjectAltName = @alt_names
|
|
||||||
keyUsage = digitalSignature, keyEncipherment
|
|
||||||
extendedKeyUsage = serverAuth
|
|
||||||
|
|
||||||
[alt_names]
|
|
||||||
DNS.1 = localhost
|
|
||||||
DNS.2 = proxy
|
|
||||||
DNS.3 = host.docker.internal
|
|
||||||
IP.1 = 127.0.0.1
|
|
||||||
IP.2 = ::1
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > /tmp/kemkas-v3.ext <<'EOF'
|
|
||||||
authorityKeyIdentifier=keyid,issuer
|
|
||||||
basicConstraints=CA:FALSE
|
|
||||||
keyUsage=digitalSignature,keyEncipherment
|
|
||||||
extendedKeyUsage=serverAuth
|
|
||||||
subjectAltName=@alt_names
|
|
||||||
|
|
||||||
[alt_names]
|
|
||||||
DNS.1=localhost
|
|
||||||
DNS.2=proxy
|
|
||||||
DNS.3=host.docker.internal
|
|
||||||
IP.1=127.0.0.1
|
|
||||||
IP.2=::1
|
|
||||||
EOF
|
|
||||||
|
|
||||||
openssl genrsa -out "$CERT_DIR/rootCA.key" 2048
|
|
||||||
openssl req -x509 -new -nodes -key "$CERT_DIR/rootCA.key" -sha256 -days 3650 -subj "/CN=kemkas-local-root" -out "$CERT_DIR/rootCA.pem"
|
|
||||||
|
|
||||||
openssl genrsa -out "$CERT_DIR/kemkas.key" 2048
|
|
||||||
openssl req -new -key "$CERT_DIR/kemkas.key" -out /tmp/kemkas.csr -config /tmp/kemkas-server.cnf
|
|
||||||
openssl x509 -req -in /tmp/kemkas.csr -CA "$CERT_DIR/rootCA.pem" -CAkey "$CERT_DIR/rootCA.key" -CAcreateserial -out "$CERT_DIR/kemkas.pem" -days 825 -sha256 -extfile /tmp/kemkas-v3.ext
|
|
||||||
|
|
||||||
echo "Generated TLS CA and nginx certificate under $CERT_DIR"
|
|
||||||
Reference in New Issue
Block a user