diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
new file mode 100644
index 0000000..18ff1c2
--- /dev/null
+++ b/.github/workflows/playwright.yml
@@ -0,0 +1,103 @@
+name: Playwright Tests
+
+on:
+ push:
+ branches: [ main, master ]
+ pull_request:
+ branches: [ main, master ]
+
+jobs:
+ playwrigth-standalone:
+ timeout-minutes: 60
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '18'
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.3'
+ extensions: yaml
+
+ - name: Install System Dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y unzip patchelf wget php-cli php-yaml gnuplot
+
+ - name: Install SunCAE Dependencies (FeenoX & Gmsh) standalone
+ run: ./deps.sh
+
+ - name: Check script
+ run: php html/check.php
+
+ - name: Install Node Dependencies
+ run: npm ci
+
+ - name: Install Playwright Browsers
+ run: npx playwright install --with-deps
+
+ - name: Run Playwright tests
+ run: npx playwright test
+
+ - name: Upload Playwright Report
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: playwright-report-standalone
+ path: playwright-report-stanadlone/
+ retention-days: 30
+
+ playwrigth-apt:
+ timeout-minutes: 60
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '18'
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.3'
+ extensions: yaml
+
+ - name: Install System Dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y unzip patchelf wget php-cli php-yaml gnuplot
+
+# feenox 1.2 is not available in ubuntu 24.04, only in 25.10
+ - name: Install SunCAE Dependencies (FeenoX & Gmsh) from apt
+ run: |
+ sudo apt-get install -y python3-gmsh pandoc
+ ./deps.sh
+
+ - name: Check script
+ run: php html/check.php
+
+ - name: Install Node Dependencies
+ run: npm ci
+
+ - name: Install Playwright Browsers
+ run: npx playwright install --with-deps
+
+ - name: Run Playwright tests
+ run: npx playwright test
+
+ - name: Upload Playwright Report
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: playwright-report-apt
+ path: playwright-report-apt/
+ retention-days: 30
diff --git a/.gitignore b/.gitignore
index ea89aa9..d1b551e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
bin
deps
data
+node_modules/
+test-results/
+playwright-report/
diff --git a/autoclean.sh b/autoclean.sh
index a7e1240..f89bf50 100644
--- a/autoclean.sh
+++ b/autoclean.sh
@@ -16,7 +16,8 @@ if [ ! -d auths ]; then
echo 1
fi
-for i in bin deps data; do
+# cat .gitgnore?
+for i in bin deps data node_modules test-results playwright-report; do
echo -n "cleaning ${i}... "
rm -rf ${i} || exit 1
echo "ok"
@@ -32,3 +33,8 @@ for i in $(find . -name .gitignore); do
cd ${pwd}
fi
done
+
+# more
+for i in x3dom.js x3dom.css; do
+ find . -name {$i} | xargs rm -f
+done
diff --git a/cadimporters/upload/gmshcheck.py b/cadimporters/upload/gmshcheck.py
deleted file mode 100755
index 0722292..0000000
--- a/cadimporters/upload/gmshcheck.py
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/usr/bin/python3
-import sys
-sys.path.append("../../../../bin")
-import gmsh
-
-gmsh.initialize()
-gmsh.finalize()
diff --git a/cadprocessors/gmsh/cad2stl.py b/cadprocessors/gmsh/cad2stl.py
index 1bb34f9..8ccd313 100755
--- a/cadprocessors/gmsh/cad2stl.py
+++ b/cadprocessors/gmsh/cad2stl.py
@@ -1,8 +1,13 @@
#!/usr/bin/python3
import sys
-sys.path.append("../../../../bin")
-import gmsh
import os
+
+script_dir = os.path.dirname(os.path.abspath(__file__))
+bin_dir = os.path.join(script_dir, '../../', 'bin') # if bin is one level up
+if os.path.exists(bin_dir):
+ sys.path.insert(0, bin_dir)
+import gmsh
+
import math
import random
import colorsys
diff --git a/cadprocessors/gmsh/cadcheck.py b/cadprocessors/gmsh/cadcheck.py
index 0a43995..8ec000c 100755
--- a/cadprocessors/gmsh/cadcheck.py
+++ b/cadprocessors/gmsh/cadcheck.py
@@ -1,8 +1,13 @@
#!/usr/bin/python3
import sys
-sys.path.append("../../../../bin")
-import gmsh
import os
+
+script_dir = os.path.dirname(os.path.abspath(__file__))
+bin_dir = os.path.join(script_dir, '../../', 'bin') # if bin is one level up
+if os.path.exists(bin_dir):
+ sys.path.insert(0, bin_dir)
+import gmsh
+
import math
import json
diff --git a/cadprocessors/gmsh/cadimport.py b/cadprocessors/gmsh/cadimport.py
index 1779891..56225f5 100755
--- a/cadprocessors/gmsh/cadimport.py
+++ b/cadprocessors/gmsh/cadimport.py
@@ -1,9 +1,14 @@
#!/usr/bin/python3
import sys
-sys.path.append("../../../../bin")
-import gmsh
import os
+
+script_dir = os.path.dirname(os.path.abspath(__file__))
+bin_dir = os.path.join(script_dir, '../../', 'bin') # if bin is one level up
+if os.path.exists(bin_dir):
+ sys.path.insert(0, bin_dir)
import gmsh
+
+import os
import math
import random
import colorsys
diff --git a/cadprocessors/gmsh/gmshcheck.py b/cadprocessors/gmsh/gmshcheck.py
new file mode 100755
index 0000000..c8beabc
--- /dev/null
+++ b/cadprocessors/gmsh/gmshcheck.py
@@ -0,0 +1,21 @@
+#!/usr/bin/python3
+import sys
+import os
+
+script_dir = os.path.dirname(os.path.abspath(__file__))
+bin_dir = os.path.join(script_dir, '../../', 'bin') # if bin is one level up
+if os.path.exists(bin_dir):
+ sys.path.insert(0, bin_dir)
+import gmsh
+
+# Initialize the Gmsh API
+gmsh.initialize()
+
+# Get the Gmsh version string
+version = gmsh.option.getString("General.Version")
+
+# Print the version
+print(version)
+
+# Finalize the Gmsh API
+gmsh.finalize()
diff --git a/cadprocessors/gmsh/process.php b/cadprocessors/gmsh/process.php
index 00154c3..a2cae73 100644
--- a/cadprocessors/gmsh/process.php
+++ b/cadprocessors/gmsh/process.php
@@ -14,16 +14,20 @@
return_error_json("username is empty");
}
-$cad_dir = "../../data/{$username}/cads/{$cad_hash}";
+$cad_dir = $data_dir . "{$username}/cads/{$cad_hash}";
if (file_exists($cad_dir) === false) {
- mkdir($cad_dir, $permissions, true);
+ if (mkdir($cad_dir, $permissions, true) === false) {
+ return_error_json("cannot mkdir {$cad_dir}");
+ }
+}
+if (chdir($cad_dir) === false) {
+ return_error_json("cannot chdir to {$cad_dir}");
}
-chdir($cad_dir);
// ------------------------------------------------------------
if (file_exists("cad.json") === false) {
exec(sprintf("%s/cadimport.py 2>&1", __DIR__), $output, $error_level);
-
+
// TODO: keep output
if ($error_level != 0) {
$error_message = "Error {$error_level} when importing CAD: ";
diff --git a/deps.sh b/deps.sh
index 7985bc0..1db9b64 100755
--- a/deps.sh
+++ b/deps.sh
@@ -19,15 +19,19 @@ for i in wget tar unzip python3; do
fi
done
+# create gitignored directories
+mkdir -p deps bin
+
+# this one needs to be either world writable or owned by the user running the web server
+# we start with 0777 but a sane admin would change it back to 0744 (or less)
if [ ! -d data ]; then
mkdir -p data
chmod 0777 data
fi
-mkdir -p deps
-# Function to compare versions (include in main deps.sh or source from a utils file)
+# Function to compare versions
version_ge() {
printf '%s\n%s\n' "$2" "$1" | sort -V -C
return $?
diff --git a/html/check.php b/html/check.php
new file mode 100644
index 0000000..bc1454b
--- /dev/null
+++ b/html/check.php
@@ -0,0 +1,200 @@
+\n";
+
+$username_output = [];
+exec('whoami', $username_output);
+$user = $username_output[0];
+
+echo "[info] username running the web server is {$user}
\n";
+
+if (file_exists($data_dir) === false) {
+ if (mkdir($data_dir, 0777) === false) {
+ echo "[error] cannot create data dir {$data_dir}
\n";
+ exit(1);
+ }
+} else {
+ if (is_dir($data_dir) === false) {
+ echo "[error] data dir exists but is not a directory
\n";
+ exit(2);
+ }
+}
+
+if (is_writable($data_dir)) {
+ echo "[good] {$data_dir} is writable by user {$user}
\n";
+} else {
+ echo "[error] {$data_dir} is not writable by user {$user}
\n";
+ exit(3);
+}
+
+// --- bin dir ----------------
+$bin_dir = __DIR__ . "/../bin";
+echo "[info] bin_dir is {$bin_dir}
\n";
+
+if (file_exists($bin_dir) && is_dir($bin_dir)) {
+ echo "[good] {$bin_dir} exists
\n";
+} else {
+ echo "[error] {$bin_dir} does not exist
\n";
+ exit(4);
+}
+
+
+
+
+// --- logging ----------------------------------
+include(__DIR__ . "/common.php");
+$username = "root";
+$err = suncae_log("running check.php script");
+
+if ($err == 0) {
+ echo "[good] logging works
\n";
+} else {
+ echo "[error] cannot create a log entry
\n";
+ exit(5);
+}
+
+
+// conf
+include(__DIR__ . "/../conf.php");
+
+// --- auth ----------------------------------
+if (file_exists(__DIR__ . "/../auths/{$auth}/auth.php")) {
+ echo "[good] auth {$auth} exists
\n";
+} else {
+ echo "[error] auth {$auth} does not exist
\n";
+ exit(6);
+}
+
+// --- ux ----------------------------------
+if (file_exists(__DIR__ . "/../uxs/{$ux}/index.php")) {
+ echo "[good] ux {$ux} exists
\n";
+} else {
+ echo "[error] ux {$ux} does not exist
\n";
+ exit(7);
+}
+
+if ($ux == "faster-than-quick") {
+ foreach (['css/bootstrap.min.css', 'css/katex.min.css', 'css/x3dom.css'] as $i) {
+ if (file_exists(__DIR__ . "/../uxs/{$ux}/{$i}")) {
+ echo "[good] {$i} exists
\n";
+ } else {
+ echo "[error] {$i} does not exist
\n";
+ exit(8);
+ }
+ }
+
+ // pandoc
+ if (file_exists("{$bin_dir}/pandoc")) {
+ echo "[good] pandoc binary exists
\n";
+ echo "[info] " . shell_exec("ls -la {$bin_dir}/pandoc") . "
\n";
+ } else {
+ echo "[error] pandoc binary does not exist
\n";
+ exit(9);
+ }
+ $exec_output = [];
+ exec("{$bin_dir}/pandoc --version 2>&1", $exec_output, $err);
+ // TODO: check version is good enough
+ if ($err == 0) {
+ echo "[good] pandoc version is {$exec_output[0]}
\n";
+ } else {
+ echo "[error] pandoc binary does not work
\n";
+ for ($i = 0; $i < count($exec_output); $i++) {
+ echo "[info] {$exec_output[$i]}
\n";
+ }
+ exit(10);
+ }
+}
+
+
+// --- cadimporter ----------------------------------
+if (file_exists(__DIR__ . "/../cadimporters/{$cadimporter}/import_cad.php")) {
+ echo "[good] cadimporter {$cadimporter} exists
\n";
+} else {
+ echo "[error] cadimporters {$cadimporter} does not exist
\n";
+ exit(11);
+}
+
+
+
+// --- cadprocessor ----------------------------------
+if (file_exists(__DIR__ . "/../cadprocessors/{$cadprocessor}/process.php")) {
+ echo "[good] cadprocessor {$cadprocessor} exists
\n";
+} else {
+ echo "[error] cadprocessor {$cadprocessor} does not exist
\n";
+ exit(12);
+}
+
+
+if ($cadprocessor == "gmsh") {
+ foreach (['css/bootstrap.min.css', 'css/katex.min.css', 'css/x3dom.css'] as $i) {
+ if (file_exists(__DIR__ . "/../uxs/{$ux}/{$i}")) {
+ echo "[good] {$i} exists
\n";
+ } else {
+ echo "[error] {$i} does not exist
\n";
+ exit(13);
+ }
+ }
+
+ // gmsh
+ if (file_exists("{$bin_dir}/gmsh")) {
+ echo "[good] gmsh binary exists
\n";
+ echo "[info] " . shell_exec("ls -la {$bin_dir}/gmsh") . "
\n";
+ } else {
+ echo "[error] gmsh binary does not exist
\n";
+ exit(14);
+ }
+ $exec_output = [];
+ exec("{$bin_dir}/gmsh -version 2>&1", $exec_output, $err);
+ // TODO: check version is good enough
+ if ($err == 0) {
+ echo "[good] gmsh version is {$exec_output[0]}
\n";
+ } else {
+ echo "[error] gmsh binary does not work
\n";
+ for ($i = 0; $i < count($exec_output); $i++) {
+ echo "[info] {$exec_output[$i]}
\n";
+ }
+ exit(15);
+ }
+
+ // python
+ $exec_output = [];
+ exec("which python", $exec_output, $err);
+ if ($err == 0) {
+ echo "[good] python binary exists at {$exec_output[0]}
\n";
+ } else {
+ echo "[error] python binary does not exist
\n";
+ exit(16);
+ }
+ $exec_output = [];
+ exec("python --version 2>&1", $exec_output, $err);
+ // TODO: check version is good enough
+ if ($err == 0) {
+ echo "[good] python version is {$exec_output[0]}
\n";
+ } else {
+ echo "[error] python binary does not work
\n";
+ for ($i = 0; $i < count($exec_output); $i++) {
+ echo "[info] {$exec_output[$i]}
\n";
+ }
+ exit(17);
+ }
+
+ // gmsh python wrapper
+ $exec_output = [];
+ exec("python " . __DIR__ . "/../cadprocessors/gmsh/gmshcheck.py 2>&1", $exec_output, $err);
+ // TODO: check version is good enough
+ if ($err == 0) {
+ echo "[good] python gmsh wrapper version is {$exec_output[0]}
\n";
+ } else {
+ echo "[error] python gmsh wrapper does not work
\n";
+ for ($i = 0; $i < count($exec_output); $i++) {
+ echo "[info] {$exec_output[$i]}
\n";
+ }
+ exit(18);
+ }
+
+ // TODO: check python and binary versions match
+
+
+}
diff --git a/html/common.php b/html/common.php
index a1e52fe..ec90dfe 100644
--- a/html/common.php
+++ b/html/common.php
@@ -3,8 +3,15 @@
// SunCAE is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
// SunCAE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
-$permissions = 0755;
+$permissions = 0777;
$id = (isset($_POST["id"])) ? $_POST["id"] : ((isset($_GET["id"])) ? $_GET["id"] : "");
+$data_dir = __DIR__ . "/../data/";
+if (file_exists($data_dir) === false) {
+ if (mkdir($data_dir, $permissions, true) === false) {
+ echo "cannot mkdir {$data_dir}, please check permissions";
+ exit(1);
+ }
+}
// based on original work from the PHP Laravel framework
if (!function_exists('str_contains')) {
@@ -16,12 +23,14 @@ function str_contains($haystack, $needle) {
function suncae_log_write($file_path, $username, $message) {
$log = fopen($file_path, "a");
- if ($log === false) {
- suncae_error("Cannot open log file, please check permissions.");
+ if ($log) {
+ $ip = (isset($_SERVER['REMOTE_ADDR'])) ? $_SERVER['REMOTE_ADDR'] : "localhost";
+ fprintf($log, "%s %s\t%s: %s\n", date("c"), $ip, $username, $message);
+ fclose($log);
+ return 0;
+ } else {
+ return 1;
}
- fprintf($log, "%s %s\t%s: %s\n", date("c"), $_SERVER['REMOTE_ADDR'], $username, $message);
- fclose($log);
-
}
function suncae_log_error($message, $level = 0) {
@@ -33,7 +42,7 @@ function suncae_log_error($message, $level = 0) {
$log_dir = __DIR__ . "/../data/logs/";
if (file_exists($log_dir) == false) {
if (mkdir($log_dir, $permissions, true) == false) {
- suncae_error("error: cannot create log directory");
+ exit(1);
}
}
@@ -43,7 +52,6 @@ function suncae_log_error($message, $level = 0) {
if ($level > 0) {
suncae_log_write("{$log_dir}0-{$date}.log", $username, $message);
}
-
}
@@ -64,25 +72,30 @@ function suncae_log($message, $level = 0) {
$log_dir = __DIR__ . "/../data/logs/";
if (file_exists($log_dir) == false) {
if (mkdir($log_dir, $permissions, true) == false) {
- suncae_error("error: cannot create log directory");
+ return 1;
}
}
$date = date('Y-m-d');
suncae_log_write("{$log_dir}0-{$date}.log", $username, $message);
if ($level > 0) {
- suncae_log_write("{$log_dir}{$level}-{$date}.log", $username, $message);
+ if (suncae_log_write("{$log_dir}{$level}-{$date}.log", $username, $message) != 0) {
+ return 1;
+ }
}
if ($username != "anonymous") {
$log_dir = __DIR__ . "/../data/{$username}/";
if (file_exists($log_dir) == false) {
if (mkdir($log_dir, $permissions, true) == false) {
- suncae_error("error: cannot create log directory");
+ return 2;
}
}
- suncae_log_write("{$log_dir}activity.log", $username, $message);
+ if (suncae_log_write("{$log_dir}activity.log", $username, $message) != 0) {
+ return 1;
+ }
}
+ return 0;
}
diff --git a/meshers/gmsh/deps.sh b/meshers/gmsh/deps.sh
index af53a80..dc93e76 100644
--- a/meshers/gmsh/deps.sh
+++ b/meshers/gmsh/deps.sh
@@ -1,7 +1,7 @@
#!/bin/false
gmsh_version=4.14.0
-gmsh_min_version=4.11.0 # Minimum required version
+gmsh_min_version=4.13.0 # Minimum required version for xao support
# Function to extract version from binary
get_gmsh_version() {
@@ -36,11 +36,13 @@ if [ $use_system_binary = 0 ]; then
echo "error: downloaded gmsh needs ${i}, please do sudo apt install patchelf"
exit 1
fi
+ mkdir -p bin
cd deps
if [ ! -e ${gmsh_tarball}.tgz ]; then
- wget -c http://gmsh.info/bin/Linux/${gmsh_tarball}.tgz
+ wget -q -c http://gmsh.info/bin/Linux/${gmsh_tarball}.tgz
fi
tar xzf ${gmsh_tarball}.tgz
+ rm -f ../bin/gmsh.py ../bin/gmsh ../bin/libgmsh.so*
cp ${gmsh_tarball}/bin/gmsh ../bin
cp ${gmsh_tarball}/lib/gmsh.py ../bin
cp -d ${gmsh_tarball}/lib/libgmsh.so* ../bin
diff --git a/meshers/gmsh/mesh_data.py b/meshers/gmsh/mesh_data.py
index 1b330fb..3b13f0a 100755
--- a/meshers/gmsh/mesh_data.py
+++ b/meshers/gmsh/mesh_data.py
@@ -1,8 +1,13 @@
#!/usr/bin/python3
import sys
-sys.path.append("../../../../bin")
-import gmsh
import os
+
+script_dir = os.path.dirname(os.path.abspath(__file__))
+bin_dir = os.path.join(script_dir, '../../', 'bin') # if bin is one level up
+if os.path.exists(bin_dir):
+ sys.path.insert(0, bin_dir)
+import gmsh
+
import json
# Add a segment, always in increasing order, to make duplicate detection easy
diff --git a/meshers/gmsh/mesh_meta.py b/meshers/gmsh/mesh_meta.py
index 8382b19..1097cec 100755
--- a/meshers/gmsh/mesh_meta.py
+++ b/meshers/gmsh/mesh_meta.py
@@ -1,9 +1,13 @@
#!/usr/bin/python3
import sys
-sys.path.append("../../../../bin")
-import gmsh
import os
-import sys
+
+script_dir = os.path.dirname(os.path.abspath(__file__))
+bin_dir = os.path.join(script_dir, '../../', 'bin') # if bin is one level up
+if os.path.exists(bin_dir):
+ sys.path.insert(0, bin_dir)
+import gmsh
+
import math
import json
diff --git a/meshers/gmsh/quality.py b/meshers/gmsh/quality.py
index d5855f1..9df9519 100644
--- a/meshers/gmsh/quality.py
+++ b/meshers/gmsh/quality.py
@@ -1,7 +1,13 @@
#!/usr/bin/python3
import sys
-sys.path.append("../../../../bin")
+import os
+
+script_dir = os.path.dirname(os.path.abspath(__file__))
+bin_dir = os.path.join(script_dir, '../../', 'bin') # if bin is one level up
+if os.path.exists(bin_dir):
+ sys.path.insert(0, bin_dir)
import gmsh
+
import math
gmsh.initialize(sys.argv)
diff --git a/meshers/gmsh/trymesh.py b/meshers/gmsh/trymesh.py
index edfe304..6a430f3 100755
--- a/meshers/gmsh/trymesh.py
+++ b/meshers/gmsh/trymesh.py
@@ -1,9 +1,14 @@
#!/usr/bin/python3
import sys
-sys.path.append("../../../../bin")
+import os
+
+script_dir = os.path.dirname(os.path.abspath(__file__))
+bin_dir = os.path.join(script_dir, '../../', 'bin') # if bin is one level up
+if os.path.exists(bin_dir):
+ sys.path.insert(0, bin_dir)
import gmsh
+
import math
-import os
import json
import signal
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..ba8c545
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,75 @@
+{
+ "name": "suncae",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "suncae",
+ "version": "1.0.0",
+ "license": "ISC",
+ "devDependencies": {
+ "@playwright/test": "^1.57.0"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz",
+ "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==",
+ "dev": true,
+ "dependencies": {
+ "playwright": "1.57.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
+ "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
+ "dev": true,
+ "dependencies": {
+ "playwright-core": "1.57.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
+ "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
+ "dev": true,
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..f9672cc
--- /dev/null
+++ b/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "suncae",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "directories": {
+ "doc": "doc"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/seamplex/suncae.git"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "bugs": {
+ "url": "https://github.com/seamplex/suncae/issues"
+ },
+ "homepage": "https://github.com/seamplex/suncae#readme",
+ "devDependencies": {
+ "@playwright/test": "^1.57.0"
+ }
+}
diff --git a/playwright.config.js b/playwright.config.js
new file mode 100644
index 0000000..a2327e3
--- /dev/null
+++ b/playwright.config.js
@@ -0,0 +1,35 @@
+// @ts-check
+const { defineConfig, devices } = require('@playwright/test');
+
+module.exports = defineConfig({
+ testDir: './tests/e2e',
+ fullyParallel: true,
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 2 : 0,
+ workers: process.env.CI ? 1 : undefined,
+ reporter: 'html',
+ use: {
+ baseURL: 'http://localhost:8000',
+ trace: 'on-first-retry',
+ video: 'retain-on-failure',
+ screenshot: 'only-on-failure',
+ // Add HAR recording to capture all network traffic
+ recordHar: {
+ path: 'test-results/network.har',
+ content: 'attach' // Include response bodies
+ },
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+
+ webServer: {
+ command: 'php -S localhost:8000 -t html',
+ url: 'http://localhost:8000',
+ reuseExistingServer: !process.env.CI,
+ },
+});
diff --git a/renderers/x3dom/deps.sh b/renderers/x3dom/deps.sh
index 826ec60..c6fef5c 100644
--- a/renderers/x3dom/deps.sh
+++ b/renderers/x3dom/deps.sh
@@ -14,13 +14,13 @@ if [ $force = 1 ] || [ ! -e renderers/x3dom/x3dom.js ]; then
# fi
# if [ ! -d x3dom ]; then
# mkdir -p x3dom
-# unzip ${x3dom_tarball}.zip -d x3dom
+# unzip -q ${x3dom_tarball}.zip -d x3dom
# fi
# cp x3dom/x3dom.js ../renderers/x3dom
# cp x3dom/x3dom.css ../renderers/x3dom
- wget -c https://andreasplesch.github.io/x3dom/dist/x3dom.js -o ../renderers/x3dom/x3dom.js
- wget -c https://andreasplesch.github.io/x3dom/dist/x3dom.css -o ../renderers/x3dom/x3dom.css
+ wget -q -c https://andreasplesch.github.io/x3dom/dist/x3dom.js -O ../renderers/x3dom/x3dom.js
+ wget -q -c https://andreasplesch.github.io/x3dom/dist/x3dom.css -O ../renderers/x3dom/x3dom.css
cd ../uxs/faster-than-quick/js
if [ ! -e x3dom.js ]; then
diff --git a/solvers/ccx/deps.sh b/solvers/ccx/deps.sh
index d4a0acc..33d23b7 100644
--- a/solvers/ccx/deps.sh
+++ b/solvers/ccx/deps.sh
@@ -10,7 +10,7 @@ ccx_tarball=ccx-${ccx_version}-linux-static
if [ $force = 1 ] || [ ! -x bin/ccx ] || [ ! -f deps/${ccx_tarball}.tar.gz ]; then
cd deps
if [ ! -e ${ccx_tarball}.tar.gz ]; then
- wget -c https://www.seamplex.com/suncae/deps/${ccx_tarball}.tar.gz
+ wget -q -c https://www.seamplex.com/suncae/deps/${ccx_tarball}.tar.gz
fi
tar xzf ${ccx_tarball}.tar.gz
cp ${ccx_tarball}/ccx ../bin
diff --git a/solvers/feenox/deps.sh b/solvers/feenox/deps.sh
index f235db6..97f8a03 100644
--- a/solvers/feenox/deps.sh
+++ b/solvers/feenox/deps.sh
@@ -1,6 +1,6 @@
#!/bin/false
-feenox_version=1.2
+feenox_version=1.2.1
feenox_version_min=1.72
# feenox
@@ -16,14 +16,14 @@ echo -n "meshers/feenox... "
use_system_binary=0
if [ -x "$(which feenox 2>/dev/null)" ] && [ $force = 0 ]; then
installed_version=$(get_feenox_version "$(which feenox)")
- if [ -n "$installed_version" ] && version_ge "$installed_version" "$feenox_min_version"; then
- echo "found system version $installed_version (>= $feenox_min_version), using it"
+ if [ -n "$installed_version" ] && version_ge "$installed_version" "$feenox_version_min"; then
+ echo "found system version $installed_version (>= $feenox_version_min), using it"
use_system_binary=1
# Create symlink to system binary
mkdir -p bin
ln -sf "$(which feenox)" bin/feenox
else
- echo "system version $installed_version is too old (need >= $feenox_min_version), will download"
+ echo "system version $installed_version is too old (need >= $feenox_version_min), will download"
fi
fi
@@ -32,7 +32,7 @@ if [ $use_system_binary = 0 ]; then
if [ $force = 1 ] || [ ! -x bin/feenox ] || [ ! -f deps/${feenox_tarball}.tgz ]; then
cd deps
if [ ! -e ${feenox_tarball}.tar.gz ]; then
- wget -c https://www.seamplex.com/feenox/dist/linux/${feenox_tarball}.tar.gz
+ wget -q -c https://www.seamplex.com/feenox/dist/linux/${feenox_tarball}.tar.gz
fi
tar xzf ${feenox_tarball}.tar.gz
cp ${feenox_tarball}/bin/feenox ../bin
diff --git a/tests/e2e/create_case.spec.js b/tests/e2e/create_case.spec.js
new file mode 100644
index 0000000..32211ba
--- /dev/null
+++ b/tests/e2e/create_case.spec.js
@@ -0,0 +1,97 @@
+// @ts-check
+const { test, expect } = require('@playwright/test');
+const path = require('path');
+
+test('create new case from sample.step', async ({ page }) => {
+ // Add network request/response logging
+ page.on('request', request => {
+ console.log('>>', request.method(), request.url());
+ });
+
+ page.on('response', async response => {
+ const url = response.url();
+ console.log('<<', response.status(), url);
+
+ // Log responses from PHP scripts involved in CAD processing
+ if (url.includes('import_cad.php') || url.includes('process.php')) {
+ try {
+ const body = await response.text();
+ console.log('Response body:', body);
+ } catch (e) {
+ console.log('Could not read response body:', e.message);
+ }
+ }
+ });
+
+ // Add browser console logging
+ page.on('console', msg => {
+ console.log('BROWSER:', msg.type(), msg.text());
+ });
+
+ // 1. Go to /new
+ await page.goto('/new/');
+
+ // 2. In that view, there's a file upload input box. Put the file html/new/sample.step in the upload box
+ // Note: path is relative to the test file.
+ // tests/e2e/create_case.spec.js -> ../../html/new/sample.step
+ const sampleFile = path.join(__dirname, '../../html/new/sample.step');
+
+ // Wait for the import response
+ const importResponsePromise = page.waitForResponse(response =>
+ response.url().includes('import_cad.php') && response.status() === 200,
+ { timeout: 30000 }
+ );
+
+ await page.setInputFiles('#cad', sampleFile);
+
+ await importResponsePromise;
+
+ // Wait for the process response
+ const processResponse = await page.waitForResponse(response =>
+ response.url().includes('process.php') && response.status() === 200,
+ { timeout: 30000 }
+ );
+
+ // Wait for the file to be processed and preview to be shown
+ try {
+ await expect(page.locator('#cad_preview')).toBeVisible({ timeout: 30000 });
+ } catch (error) {
+ // Check if there's an error message displayed
+ const errorDiv = page.locator('#cad_error');
+ if (await errorDiv.isVisible()) {
+ const errorText = await errorDiv.textContent();
+ console.log('CAD Error displayed:', errorText);
+ }
+
+ // Take a screenshot for debugging
+ await page.screenshot({ path: 'test-results/upload-failure.png', fullPage: true });
+
+ throw error;
+ }
+ await page.setInputFiles('#cad', sampleFile);
+
+ // Wait for the file to be processed and preview to be shown
+ await expect(page.locator('#cad_preview')).toBeVisible({ timeout: 30000 });
+
+ // 3. Select "Solid mechanics" in the combo box for physics
+ await page.selectOption('#physics', 'solid');
+
+ // 4. Select "Mechanical elasticty" in the combo box for problem
+ await page.selectOption('#problem', 'mechanical');
+
+ // 5. Select "FeenoX" in the combo box for solver
+ await page.selectOption('#solver', 'feenox');
+
+ // 6. Select "Gmsh" in the combo box for mesher
+ // 6. Select "Gmsh" in thecombo box for mesher
+ await page.selectOption('#mesher', 'gmsh');
+
+ // Verify start button is enabled before clicking
+ await expect(page.locator('#btn_start')).toBeEnabled();
+
+ // 7. Click "Start"
+ await page.click('#btn_start');
+
+ // Optional: Verify we moved to the next step (e.g., URL changes to create.php or creating a case)
+ // For now, just ensuring no error occurs immediately after click.
+});
diff --git a/tests/e2e/example.spec.js b/tests/e2e/example.spec.js
new file mode 100644
index 0000000..f36a88d
--- /dev/null
+++ b/tests/e2e/example.spec.js
@@ -0,0 +1,9 @@
+// @ts-check
+const { test, expect } = require('@playwright/test');
+
+test('homepage has title', async ({ page }) => {
+ await page.goto('/');
+
+ // Expect a title "to contain" a substring.
+ await expect(page).toHaveTitle(/Faster-than-quick/);
+});
diff --git a/uxs/faster-than-quick/about.php b/uxs/faster-than-quick/about.php
index 7f3134d..be6f335 100644
--- a/uxs/faster-than-quick/about.php
+++ b/uxs/faster-than-quick/about.php
@@ -18,16 +18,15 @@