testeranto 0.199.0 → 0.200.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/test_runner +0 -0
- package/bin/testeranto +0 -0
- package/bundle.js +5 -4
- package/cmd/test_runner/main.go +65 -0
- package/cmd/testeranto/main.go +37 -0
- package/dist/common/src/PM/main.js +126 -11
- package/dist/common/src/PM/pitonoRunner.js +54 -0
- package/dist/common/src/components/pure/TestPageView.js +180 -65
- package/dist/common/src/components/stateful/TestPage.js +50 -11
- package/dist/common/src/lib/abstractBase.test/index.js +1 -0
- package/dist/common/src/run.js +48 -82
- package/dist/common/src/{build.js → testeranto.js} +107 -55
- package/dist/common/src/utils/golingvuMetafile.js +116 -0
- package/dist/common/src/utils/logFiles.js +2 -1
- package/dist/common/src/utils/pitonoMetafile.js +67 -0
- package/dist/common/src/utils.js +40 -1
- package/dist/common/testeranto.config.js +23 -21
- package/dist/common/tsconfig.common.tsbuildinfo +1 -1
- package/dist/module/src/PM/main.js +126 -11
- package/dist/module/src/PM/pitonoRunner.js +47 -0
- package/dist/module/src/components/pure/TestPageView.js +180 -65
- package/dist/module/src/components/stateful/TestPage.js +50 -11
- package/dist/module/src/lib/abstractBase.test/index.js +1 -0
- package/dist/module/src/run.js +49 -45
- package/dist/module/src/{build.js → testeranto.js} +107 -55
- package/dist/module/src/utils/golingvuMetafile.js +109 -0
- package/dist/module/src/utils/logFiles.js +2 -1
- package/dist/module/src/utils/pitonoMetafile.js +60 -0
- package/dist/module/src/utils.js +40 -1
- package/dist/module/testeranto.config.js +23 -21
- package/dist/module/tsconfig.module.tsbuildinfo +1 -1
- package/dist/prebuild/App.js +81 -17
- package/dist/prebuild/testeranto.mjs +3249 -0
- package/dist/types/src/PM/main.d.ts +2 -0
- package/dist/types/src/PM/pitonoRunner.d.ts +7 -0
- package/dist/types/src/Types.d.ts +1 -1
- package/dist/types/src/run.d.ts +0 -1
- package/dist/types/src/utils/golingvuMetafile.d.ts +19 -0
- package/dist/types/src/utils/logFiles.d.ts +5 -1
- package/dist/types/src/utils/pitonoMetafile.d.ts +7 -0
- package/dist/types/src/utils.d.ts +5 -0
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/docs/index.md +13 -13
- package/example/test_example.py +106 -0
- package/go.mod +3 -0
- package/package.json +2 -2
- package/pitono/__init__.py +54 -0
- package/pitono/base_given.py +131 -0
- package/pitono/base_suite.py +95 -0
- package/pitono/base_then.py +50 -0
- package/pitono/base_when.py +52 -0
- package/pitono/core_generator.py +110 -0
- package/pitono/pitono.egg-info/PKG-INFO +17 -0
- package/pitono/pitono.egg-info/SOURCES.txt +7 -0
- package/pitono/pitono.egg-info/dependency_links.txt +1 -0
- package/pitono/pitono.egg-info/entry_points.txt +2 -0
- package/pitono/pitono.egg-info/top_level.txt +1 -0
- package/pitono/pyproject.toml +26 -0
- package/pitono/setup.py +40 -0
- package/pitono/simple_adapter.py +24 -0
- package/pitono/types.py +78 -0
- package/sampleMetafile.json +56 -0
- package/src/PM/main.ts +146 -17
- package/src/PM/pitonoRunner.ts +49 -0
- package/src/Types.ts +1 -1
- package/src/components/pure/TestPageView.tsx +175 -8
- package/src/components/stateful/TestPage.tsx +57 -16
- package/src/core/types.go +36 -0
- package/src/golingvu/README.md +3 -0
- package/src/golingvu/base_given.go +76 -0
- package/src/golingvu/base_suite.go +39 -0
- package/src/golingvu/base_suite_test.go +197 -0
- package/src/golingvu/base_then.go +21 -0
- package/src/golingvu/base_when.go +21 -0
- package/src/golingvu/golingvu.go +179 -0
- package/src/golingvu/test_adapter.go +33 -0
- package/src/golingvu/types.go +86 -0
- package/src/lib/abstractBase.test/index.ts +1 -0
- package/src/pitono/README.md +3 -0
- package/src/run.ts +48 -48
- package/src/templates/frontpage.html +26 -17
- package/src/{build.ts → testeranto.ts} +128 -58
- package/src/utils/golingvuMetafile.ts +165 -0
- package/src/utils/logFiles.ts +2 -1
- package/src/utils/pitonoMetafile.ts +68 -0
- package/src/utils.ts +38 -1
- package/testeranto/App.js +81 -17
- package/testeranto/metafiles/golang/core.json +72 -0
- package/testeranto/metafiles/node/core.json +21 -459
- package/testeranto/metafiles/pure/core.json +18 -119
- package/testeranto/metafiles/web/core.json +37 -16797
- package/testeranto/reports/core/config.json +8 -40
- package/testeranto/reports/core/src/lib/BaseSuite.test/node.test/node/lint_errors.txt +6 -0
- package/testeranto/reports/core/src/lib/BaseSuite.test/node.test/node/prompt.txt +12 -1
- package/testeranto/reports/core/src/lib/BaseSuite.test/pure.test/pure/lint_errors.txt +2 -0
- package/testeranto/reports/core/src/lib/BaseSuite.test/pure.test/pure/prompt.txt +11 -1
- package/testeranto/reports/core/src/lib/BaseSuite.test/web.test/web/lint_errors.txt +2 -0
- package/testeranto/reports/core/src/lib/BaseSuite.test/web.test/web/prompt.txt +13 -3
- package/testeranto/reports/core/summary.json +9 -45
- package/testeranto.config.ts +25 -21
- package/tsc.log +46 -7
- package/dist/common/src/lib/mocks.test.js +0 -11
- package/dist/module/src/lib/mocks.test.js +0 -11
- package/dist/prebuild/ReportServer.mjs +0 -227
- package/dist/prebuild/build.mjs +0 -578
- package/dist/prebuild/mothership/index.mjs +0 -22
- package/dist/prebuild/run.mjs +0 -2290
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/dist/types/src/lib/mocks.test.d.ts +0 -0
- package/src/lib/mocks.test.ts +0 -11
- package/testeranto/reports/core/src/Pure.test/pure/exit.log +0 -0
- package/testeranto/reports/core/src/Pure.test/pure/lint_errors.txt +0 -0
- package/testeranto/reports/core/src/Pure.test/pure/message.txt +0 -17
- package/testeranto/reports/core/src/Pure.test/pure/prompt.txt +0 -14
- package/testeranto/reports/core/src/Pure.test/pure/type_errors.txt +0 -66
- package/testeranto/reports/core/src/components/pure/FeaturesReporterView.test/index/web/debug.log +0 -0
- package/testeranto/reports/core/src/components/pure/FeaturesReporterView.test/index/web/error.log +0 -67
- package/testeranto/reports/core/src/components/pure/FeaturesReporterView.test/index/web/exit.log +0 -1
- package/testeranto/reports/core/src/components/pure/FeaturesReporterView.test/index/web/info.log +0 -2
- package/testeranto/reports/core/src/components/pure/FeaturesReporterView.test/index/web/lint_errors.txt +0 -0
- package/testeranto/reports/core/src/components/pure/FeaturesReporterView.test/index/web/message.txt +0 -17
- package/testeranto/reports/core/src/components/pure/FeaturesReporterView.test/index/web/prompt.txt +0 -16
- package/testeranto/reports/core/src/components/pure/FeaturesReporterView.test/index/web/tests.json +0 -68
- package/testeranto/reports/core/src/components/pure/FeaturesReporterView.test/index/web/type_errors.txt +0 -56
- package/testeranto/reports/core/src/components/pure/FeaturesReporterView.test/index/web/warn.log +0 -0
- package/testeranto/reports/core/src/components/pure/ProjectPageView.test/index/web/debug.log +0 -0
- package/testeranto/reports/core/src/components/pure/ProjectPageView.test/index/web/error.log +0 -22
- package/testeranto/reports/core/src/components/pure/ProjectPageView.test/index/web/exit.log +0 -1
- package/testeranto/reports/core/src/components/pure/ProjectPageView.test/index/web/info.log +0 -2
- package/testeranto/reports/core/src/components/pure/ProjectPageView.test/index/web/lint_errors.txt +0 -13
- package/testeranto/reports/core/src/components/pure/ProjectPageView.test/index/web/message.txt +0 -17
- package/testeranto/reports/core/src/components/pure/ProjectPageView.test/index/web/prompt.txt +0 -16
- package/testeranto/reports/core/src/components/pure/ProjectPageView.test/index/web/tests.json +0 -88
- package/testeranto/reports/core/src/components/pure/ProjectPageView.test/index/web/type_errors.txt +0 -45
- package/testeranto/reports/core/src/components/pure/ProjectPageView.test/index/web/warn.log +0 -0
- package/testeranto/reports/core/src/components/pure/TestPageView.test/index/web/debug.log +0 -0
- package/testeranto/reports/core/src/components/pure/TestPageView.test/index/web/error.log +0 -0
- package/testeranto/reports/core/src/components/pure/TestPageView.test/index/web/exit.log +0 -1
- package/testeranto/reports/core/src/components/pure/TestPageView.test/index/web/info.log +0 -2
- package/testeranto/reports/core/src/components/pure/TestPageView.test/index/web/lint_errors.txt +0 -47
- package/testeranto/reports/core/src/components/pure/TestPageView.test/index/web/message.txt +0 -17
- package/testeranto/reports/core/src/components/pure/TestPageView.test/index/web/prompt.txt +0 -17
- package/testeranto/reports/core/src/components/pure/TestPageView.test/index/web/tests.json +0 -57
- package/testeranto/reports/core/src/components/pure/TestPageView.test/index/web/type_errors.txt +0 -99
- package/testeranto/reports/core/src/components/pure/TestPageView.test/index/web/warn.log +0 -0
- package/testeranto/reports/core/src/lib/TipoSkripto.test/TipoSkripto/node/exit.log +0 -1
- package/testeranto/reports/core/src/lib/TipoSkripto.test/TipoSkripto/node/lint_errors.txt +0 -0
- package/testeranto/reports/core/src/lib/TipoSkripto.test/TipoSkripto/node/message.txt +0 -17
- package/testeranto/reports/core/src/lib/TipoSkripto.test/TipoSkripto/node/prompt.txt +0 -17
- package/testeranto/reports/core/src/lib/TipoSkripto.test/TipoSkripto/node/stderr.log +0 -18
- package/testeranto/reports/core/src/lib/TipoSkripto.test/TipoSkripto/node/stdout.log +0 -0
- package/testeranto/reports/core/src/lib/TipoSkripto.test/TipoSkripto/node/type_errors.txt +0 -32
- package/testeranto/reports/core/src/lib/pmProxy.test/index/node/exit.log +0 -1
- package/testeranto/reports/core/src/lib/pmProxy.test/index/node/lint_errors.txt +0 -15
- package/testeranto/reports/core/src/lib/pmProxy.test/index/node/message.txt +0 -17
- package/testeranto/reports/core/src/lib/pmProxy.test/index/node/prompt.txt +0 -17
- package/testeranto/reports/core/src/lib/pmProxy.test/index/node/stderr.log +0 -66
- package/testeranto/reports/core/src/lib/pmProxy.test/index/node/stdout.log +0 -10
- package/testeranto/reports/core/src/lib/pmProxy.test/index/node/type_errors.txt +0 -47
- /package/dist/types/src/{build.d.ts → testeranto.d.ts} +0 -0
|
@@ -48,6 +48,13 @@ function runtimeLogs(runtime, reportDest) {
|
|
|
48
48
|
exit: fs.createWriteStream(`${safeDest}/exit.log`),
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
|
+
else if (runtime === "pitono") {
|
|
52
|
+
return {
|
|
53
|
+
stdout: fs.createWriteStream(`${safeDest}/stdout.log`),
|
|
54
|
+
stderr: fs.createWriteStream(`${safeDest}/stderr.log`),
|
|
55
|
+
exit: fs.createWriteStream(`${safeDest}/exit.log`),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
51
58
|
else {
|
|
52
59
|
throw `unknown runtime: ${runtime}`;
|
|
53
60
|
}
|
|
@@ -780,6 +787,32 @@ export class PM_Main extends PM_WithEslintAndTsc {
|
|
|
780
787
|
// }
|
|
781
788
|
// }
|
|
782
789
|
};
|
|
790
|
+
this.launchPitono = async (src, dest) => {
|
|
791
|
+
console.log(ansiC.green(ansiC.inverse(`pitono < ${src}`)));
|
|
792
|
+
this.bddTestIsRunning(src);
|
|
793
|
+
const reportDest = `testeranto/reports/${this.name}/${src
|
|
794
|
+
.split(".")
|
|
795
|
+
.slice(0, -1)
|
|
796
|
+
.join(".")}/pitono`;
|
|
797
|
+
if (!fs.existsSync(reportDest)) {
|
|
798
|
+
fs.mkdirSync(reportDest, { recursive: true });
|
|
799
|
+
}
|
|
800
|
+
const logs = createLogStreams(reportDest, "node"); // Use node-style logs for pitono
|
|
801
|
+
try {
|
|
802
|
+
// Execute the Python test using the pitono runner
|
|
803
|
+
const { PitonoRunner } = await import('./pitonoRunner');
|
|
804
|
+
const runner = new PitonoRunner(this.configs, this.name);
|
|
805
|
+
await runner.run();
|
|
806
|
+
this.bddTestIsNowDone(src, 0);
|
|
807
|
+
statusMessagePretty(0, src, "pitono");
|
|
808
|
+
}
|
|
809
|
+
catch (error) {
|
|
810
|
+
logs.writeExitCode(-1, error);
|
|
811
|
+
console.log(ansiC.red(ansiC.inverse(`${src} errored with: ${error}. Check logs for more info`)));
|
|
812
|
+
this.bddTestIsNowDone(src, -1);
|
|
813
|
+
statusMessagePretty(-1, src, "pitono");
|
|
814
|
+
}
|
|
815
|
+
};
|
|
783
816
|
this.launchWeb = async (src, dest) => {
|
|
784
817
|
console.log(ansiC.green(ansiC.inverse(`web < ${src}`)));
|
|
785
818
|
this.bddTestIsRunning(src);
|
|
@@ -1352,7 +1385,7 @@ import('${d}').then(async (x) => {
|
|
|
1352
1385
|
console.error(e);
|
|
1353
1386
|
console.error("could not start chrome via puppeter. Check this path: ", executablePath);
|
|
1354
1387
|
}
|
|
1355
|
-
const { nodeEntryPoints, webEntryPoints, pureEntryPoints } = this.getRunnables(this.configs.tests, this.name);
|
|
1388
|
+
const { nodeEntryPoints, webEntryPoints, pureEntryPoints, pitonoEntryPoints } = this.getRunnables(this.configs.tests, this.name);
|
|
1356
1389
|
[
|
|
1357
1390
|
[
|
|
1358
1391
|
nodeEntryPoints,
|
|
@@ -1378,9 +1411,31 @@ import('${d}').then(async (x) => {
|
|
|
1378
1411
|
this.importMetafileWatcher = w;
|
|
1379
1412
|
},
|
|
1380
1413
|
],
|
|
1414
|
+
[
|
|
1415
|
+
pitonoEntryPoints,
|
|
1416
|
+
this.launchPitono,
|
|
1417
|
+
"pitono",
|
|
1418
|
+
(w) => {
|
|
1419
|
+
this.pitonoMetafileWatcher = w;
|
|
1420
|
+
},
|
|
1421
|
+
],
|
|
1381
1422
|
].forEach(async ([eps, launcher, runtime, watcher]) => {
|
|
1382
|
-
|
|
1383
|
-
|
|
1423
|
+
let metafile;
|
|
1424
|
+
if (runtime === "pitono") {
|
|
1425
|
+
metafile = `./testeranto/metafiles/python/core.json`;
|
|
1426
|
+
// Ensure the directory exists before trying to watch
|
|
1427
|
+
const metafileDir = path.dirname(metafile);
|
|
1428
|
+
if (!fs.existsSync(metafileDir)) {
|
|
1429
|
+
fs.mkdirSync(metafileDir, { recursive: true });
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
else {
|
|
1433
|
+
metafile = `./testeranto/metafiles/${runtime}/${this.name}.json`;
|
|
1434
|
+
}
|
|
1435
|
+
// Only poll for file if it's not a pitono runtime
|
|
1436
|
+
if (runtime !== "pitono") {
|
|
1437
|
+
await pollForFile(metafile);
|
|
1438
|
+
}
|
|
1384
1439
|
Object.entries(eps).forEach(async ([inputFile, outputFile]) => {
|
|
1385
1440
|
// await pollForFile(outputFile);\
|
|
1386
1441
|
this.launchers[inputFile] = () => launcher(inputFile, outputFile);
|
|
@@ -1401,10 +1456,37 @@ import('${d}').then(async (x) => {
|
|
|
1401
1456
|
}
|
|
1402
1457
|
});
|
|
1403
1458
|
this.metafileOutputs(runtime);
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1459
|
+
// For pitono, we need to wait for the file to be created
|
|
1460
|
+
if (runtime === "pitono") {
|
|
1461
|
+
// Use polling to wait for the file to exist
|
|
1462
|
+
const checkFileExists = () => {
|
|
1463
|
+
if (fs.existsSync(metafile)) {
|
|
1464
|
+
console.log(ansiC.green(ansiC.inverse(`Pitono metafile found: ${metafile}`)));
|
|
1465
|
+
// Set up the watcher once the file exists
|
|
1466
|
+
watcher(watch(metafile, async (e, filename) => {
|
|
1467
|
+
console.log(ansiC.yellow(ansiC.inverse(`< ${e} ${filename} (${runtime})`)));
|
|
1468
|
+
this.metafileOutputs(runtime);
|
|
1469
|
+
}));
|
|
1470
|
+
// Read the metafile immediately
|
|
1471
|
+
this.metafileOutputs(runtime);
|
|
1472
|
+
}
|
|
1473
|
+
else {
|
|
1474
|
+
// Check again after a delay
|
|
1475
|
+
setTimeout(checkFileExists, 1000);
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
// Start checking for the file
|
|
1479
|
+
checkFileExists();
|
|
1480
|
+
}
|
|
1481
|
+
else {
|
|
1482
|
+
// For other runtimes, only set up watcher if the file exists
|
|
1483
|
+
if (fs.existsSync(metafile)) {
|
|
1484
|
+
watcher(watch(metafile, async (e, filename) => {
|
|
1485
|
+
console.log(ansiC.yellow(ansiC.inverse(`< ${e} ${filename} (${runtime})`)));
|
|
1486
|
+
this.metafileOutputs(runtime);
|
|
1487
|
+
}));
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1408
1490
|
});
|
|
1409
1491
|
// Object.keys(this.configs.externalTests).forEach((et) => {
|
|
1410
1492
|
// this.launchExternalTest(et, this.configs.externalTests[et]);
|
|
@@ -1446,6 +1528,9 @@ import('${d}').then(async (x) => {
|
|
|
1446
1528
|
this.nodeMetafileWatcher.close();
|
|
1447
1529
|
this.webMetafileWatcher.close();
|
|
1448
1530
|
this.importMetafileWatcher.close();
|
|
1531
|
+
if (this.pitonoMetafileWatcher) {
|
|
1532
|
+
this.pitonoMetafileWatcher.close();
|
|
1533
|
+
}
|
|
1449
1534
|
// Close any remaining log streams
|
|
1450
1535
|
Object.values(this.logStreams || {}).forEach((logs) => logs.closeAll());
|
|
1451
1536
|
// Close WebSocket server
|
|
@@ -1464,11 +1549,41 @@ import('${d}').then(async (x) => {
|
|
|
1464
1549
|
this.checkForShutdown();
|
|
1465
1550
|
}
|
|
1466
1551
|
async metafileOutputs(platform) {
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
.
|
|
1470
|
-
|
|
1552
|
+
let metafilePath;
|
|
1553
|
+
if (platform === "pitono") {
|
|
1554
|
+
metafilePath = `./testeranto/metafiles/python/core.json`;
|
|
1555
|
+
}
|
|
1556
|
+
else {
|
|
1557
|
+
metafilePath = `./testeranto/metafiles/${platform}/${this.name}.json`;
|
|
1558
|
+
}
|
|
1559
|
+
// Check if the file exists
|
|
1560
|
+
if (!fs.existsSync(metafilePath)) {
|
|
1561
|
+
if (platform === "pitono") {
|
|
1562
|
+
console.log(ansiC.yellow(ansiC.inverse(`Pitono metafile not found yet: ${metafilePath}`)));
|
|
1563
|
+
}
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
let metafile;
|
|
1567
|
+
try {
|
|
1568
|
+
const fileContent = fs.readFileSync(metafilePath).toString();
|
|
1569
|
+
const parsedData = JSON.parse(fileContent);
|
|
1570
|
+
// Handle different metafile structures
|
|
1571
|
+
if (platform === "pitono") {
|
|
1572
|
+
// Pitono metafile might be the entire content or have a different structure
|
|
1573
|
+
metafile = parsedData.metafile || parsedData;
|
|
1574
|
+
}
|
|
1575
|
+
else {
|
|
1576
|
+
metafile = parsedData.metafile;
|
|
1577
|
+
}
|
|
1578
|
+
if (!metafile) {
|
|
1579
|
+
console.log(ansiC.yellow(ansiC.inverse(`No metafile found in ${metafilePath}`)));
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
catch (error) {
|
|
1584
|
+
console.error(`Error reading metafile at ${metafilePath}:`, error);
|
|
1471
1585
|
return;
|
|
1586
|
+
}
|
|
1472
1587
|
const outputs = metafile.outputs;
|
|
1473
1588
|
Object.keys(outputs).forEach(async (k) => {
|
|
1474
1589
|
const pattern = `testeranto/bundles/${platform}/${this.name}/${this.configs.src}`;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
export class PitonoRunner {
|
|
5
|
+
constructor(config, testName) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
this.testName = testName;
|
|
8
|
+
}
|
|
9
|
+
async run() {
|
|
10
|
+
const coreJsonPath = path.join(process.cwd(), 'testeranto', 'pitono', this.testName, 'core.json');
|
|
11
|
+
// Wait for the core.json file to be created with a timeout
|
|
12
|
+
const maxWaitTime = 10000; // 10 seconds
|
|
13
|
+
const startTime = Date.now();
|
|
14
|
+
while (!fs.existsSync(coreJsonPath) && (Date.now() - startTime) < maxWaitTime) {
|
|
15
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
16
|
+
}
|
|
17
|
+
if (!fs.existsSync(coreJsonPath)) {
|
|
18
|
+
console.error(`Pitono core.json not found at: ${coreJsonPath} after waiting ${maxWaitTime}ms`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const coreData = JSON.parse(fs.readFileSync(coreJsonPath, 'utf-8'));
|
|
23
|
+
const entryPoints = coreData.entryPoints;
|
|
24
|
+
for (const entryPoint of entryPoints) {
|
|
25
|
+
try {
|
|
26
|
+
console.log(`Running pitono test: ${entryPoint}`);
|
|
27
|
+
// Use python to execute the test file
|
|
28
|
+
const absolutePath = path.resolve(entryPoint);
|
|
29
|
+
// Check if the file exists
|
|
30
|
+
if (!fs.existsSync(absolutePath)) {
|
|
31
|
+
console.error(`Pitono test file not found: ${absolutePath}`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
execSync(`python "${absolutePath}"`, { stdio: 'inherit' });
|
|
35
|
+
console.log(`Pitono test completed: ${entryPoint}`);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
console.error(`Pitono test failed: ${entryPoint}`, error);
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error(`Error reading or parsing core.json: ${error}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -44,9 +44,44 @@ export const TestPageView = ({ projectName, testName, decodedTestPath, runtime,
|
|
|
44
44
|
const [expandedSections, setExpandedSections] = useState({
|
|
45
45
|
standardLogs: true,
|
|
46
46
|
runtimeLogs: true,
|
|
47
|
-
sourceFiles: true
|
|
47
|
+
sourceFiles: true,
|
|
48
|
+
buildErrors: true,
|
|
48
49
|
});
|
|
49
50
|
const [isNavbarCollapsed, setIsNavbarCollapsed] = useState(false);
|
|
51
|
+
// Extract build errors and warnings relevant to this test
|
|
52
|
+
const [buildErrors, setBuildErrors] = useState({ errors: [], warnings: [] });
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
var _a, _b, _c;
|
|
55
|
+
const metafile = (_a = logs.build_logs) === null || _a === void 0 ? void 0 : _a.metafile;
|
|
56
|
+
if (!metafile) {
|
|
57
|
+
setBuildErrors({ errors: [], warnings: [] });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const sourceFilesSet = new Set();
|
|
61
|
+
// Collect all input files from metafile outputs related to this test
|
|
62
|
+
Object.entries(metafile.outputs || {}).forEach(([outputPath, output]) => {
|
|
63
|
+
// Normalize paths for comparison
|
|
64
|
+
const normalizedTestName = testName.replace(/\\/g, '/');
|
|
65
|
+
const normalizedEntryPoint = output.entryPoint ? output.entryPoint.replace(/\\/g, '/') : '';
|
|
66
|
+
if (normalizedEntryPoint.includes(normalizedTestName)) {
|
|
67
|
+
Object.keys(output.inputs || {}).forEach((inputPath) => {
|
|
68
|
+
sourceFilesSet.add(inputPath.replace(/\\/g, '/'));
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
// Filter errors and warnings to those originating from source files of this test
|
|
73
|
+
const filteredErrors = (((_b = logs.build_logs) === null || _b === void 0 ? void 0 : _b.errors) || []).filter((err) => {
|
|
74
|
+
if (!err.location || !err.location.file)
|
|
75
|
+
return false;
|
|
76
|
+
return sourceFilesSet.has(err.location.file.replace(/\\/g, '/'));
|
|
77
|
+
});
|
|
78
|
+
const filteredWarnings = (((_c = logs.build_logs) === null || _c === void 0 ? void 0 : _c.warnings) || []).filter((warn) => {
|
|
79
|
+
if (!warn.location || !warn.location.file)
|
|
80
|
+
return false;
|
|
81
|
+
return sourceFilesSet.has(warn.location.file.replace(/\\/g, '/'));
|
|
82
|
+
});
|
|
83
|
+
setBuildErrors({ errors: filteredErrors, warnings: filteredWarnings });
|
|
84
|
+
}, [logs, testName]);
|
|
50
85
|
// Update customMessage when logs change
|
|
51
86
|
useEffect(() => {
|
|
52
87
|
if (typeof logs['message.txt'] === 'string' && logs['message.txt'].trim()) {
|
|
@@ -87,67 +122,94 @@ export const TestPageView = ({ projectName, testName, decodedTestPath, runtime,
|
|
|
87
122
|
}
|
|
88
123
|
};
|
|
89
124
|
const renderTestResults = (testData) => {
|
|
90
|
-
return (React.createElement("div", { className: "test-results" },
|
|
91
|
-
React.createElement("div", { className: "
|
|
92
|
-
React.createElement("div", { className: "
|
|
93
|
-
React.createElement("div",
|
|
94
|
-
React.createElement("h4", null,
|
|
95
|
-
"Given: ",
|
|
96
|
-
given.name),
|
|
97
|
-
given.features && given.features.length > 0 && (React.createElement("div", { className: "mt-1" },
|
|
98
|
-
React.createElement("small", null, "Features:"),
|
|
99
|
-
React.createElement("ul", { className: "list-unstyled" }, given.features.map((feature, fi) => (React.createElement("li", { key: fi }, feature.startsWith("http") ? (React.createElement("a", { href: feature, target: "_blank", rel: "noopener noreferrer", className: "text-white" }, new URL(feature).hostname)) : (React.createElement("span", { className: "text-white" }, feature))))))))),
|
|
100
|
-
given.artifacts && given.artifacts.length > 0 && (React.createElement("div", { className: "dropdown" },
|
|
101
|
-
React.createElement("button", { className: "btn btn-sm btn-light dropdown-toggle", type: "button", "data-bs-toggle": "dropdown" },
|
|
102
|
-
"Artifacts (",
|
|
103
|
-
given.artifacts.length,
|
|
104
|
-
")"),
|
|
105
|
-
React.createElement("ul", { className: "dropdown-menu dropdown-menu-end" }, given.artifacts.map((artifact, ai) => (React.createElement("li", { key: ai },
|
|
106
|
-
React.createElement("a", { className: "dropdown-item", href: `reports/${projectName}/${testName
|
|
107
|
-
.split(".")
|
|
108
|
-
.slice(0, -1)
|
|
109
|
-
.join(".")}/${runtime}/${artifact}`, target: "_blank", rel: "noopener noreferrer" }, artifact.split("/").pop()))))))))),
|
|
110
|
-
React.createElement("div", { className: "card-body" },
|
|
111
|
-
given.whens.map((when, j) => (React.createElement("div", { key: `w-${j}`, className: `p-3 mb-2 ${when.error
|
|
112
|
-
? "bg-danger text-white"
|
|
113
|
-
: "bg-success text-white"}` },
|
|
114
|
-
React.createElement("div", { className: "d-flex justify-content-between align-items-start" },
|
|
125
|
+
return (React.createElement("div", { className: "test-results" },
|
|
126
|
+
testData.givens.map((given, i) => (React.createElement("div", { key: i, className: "mb-4 card" },
|
|
127
|
+
React.createElement("div", { className: "card-header bg-primary text-white" },
|
|
128
|
+
React.createElement("div", { className: "d-flex justify-content-between align-items-center" },
|
|
115
129
|
React.createElement("div", null,
|
|
116
|
-
React.createElement("
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
130
|
+
React.createElement("h4", null,
|
|
131
|
+
"Given: ",
|
|
132
|
+
given.name),
|
|
133
|
+
given.features && given.features.length > 0 && (React.createElement("div", { className: "mt-1" },
|
|
134
|
+
React.createElement("small", null, "Features:"),
|
|
135
|
+
React.createElement("ul", { className: "list-unstyled" }, given.features.map((feature, fi) => (React.createElement("li", { key: fi }, feature.startsWith("http") ? (React.createElement("a", { href: feature, target: "_blank", rel: "noopener noreferrer", className: "text-white" }, new URL(feature).hostname)) : (React.createElement("span", { className: "text-white" }, feature))))))))),
|
|
136
|
+
given.artifacts && given.artifacts.length > 0 && (React.createElement("div", { className: "dropdown" },
|
|
137
|
+
React.createElement("button", { className: "btn btn-sm btn-light dropdown-toggle", type: "button", "data-bs-toggle": "dropdown" },
|
|
138
|
+
"Artifacts (",
|
|
139
|
+
given.artifacts.length,
|
|
140
|
+
")"),
|
|
141
|
+
React.createElement("ul", { className: "dropdown-menu dropdown-menu-end" }, given.artifacts.map((artifact, ai) => (React.createElement("li", { key: ai },
|
|
142
|
+
React.createElement("a", { className: "dropdown-item", href: `reports/${projectName}/${testName
|
|
128
143
|
.split(".")
|
|
129
144
|
.slice(0, -1)
|
|
130
|
-
.join(".")}/${runtime}/${artifact}`, target: "_blank",
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
React.createElement("div",
|
|
145
|
+
.join(".")}/${runtime}/${artifact}`, target: "_blank", rel: "noopener noreferrer" }, artifact.split("/").pop()))))))))),
|
|
146
|
+
React.createElement("div", { className: "card-body" },
|
|
147
|
+
given.whens.map((when, j) => (React.createElement("div", { key: `w-${j}`, className: `p-3 mb-2 ${when.error
|
|
148
|
+
? "bg-danger text-white"
|
|
149
|
+
: "bg-success text-white"}` },
|
|
150
|
+
React.createElement("div", { className: "d-flex justify-content-between align-items-start" },
|
|
136
151
|
React.createElement("div", null,
|
|
137
|
-
React.createElement("
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
React.createElement("
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
React.createElement("
|
|
146
|
-
|
|
147
|
-
React.createElement("
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
React.createElement("div", null,
|
|
153
|
+
React.createElement("strong", null, "When:"),
|
|
154
|
+
" ",
|
|
155
|
+
when.name,
|
|
156
|
+
when.features && when.features.length > 0 && (React.createElement("div", { className: "mt-2" },
|
|
157
|
+
React.createElement("small", null, "Features:"),
|
|
158
|
+
React.createElement("ul", { className: "list-unstyled" }, when.features.map((feature, fi) => (React.createElement("li", { key: fi }, feature.startsWith("http") ? (React.createElement("a", { href: feature, target: "_blank", rel: "noopener noreferrer" }, new URL(feature).hostname)) : (feature))))))),
|
|
159
|
+
when.error && React.createElement("pre", { className: "mt-2" }, when.error))),
|
|
160
|
+
when.artifacts && when.artifacts.length > 0 && (React.createElement("div", { className: "ms-3" },
|
|
161
|
+
React.createElement("strong", null, "Artifacts:"),
|
|
162
|
+
React.createElement("ul", { className: "list-unstyled" }, when.artifacts.map((artifact, ai) => (React.createElement("li", { key: ai },
|
|
163
|
+
React.createElement("a", { href: `reports/${projectName}/${testName
|
|
164
|
+
.split(".")
|
|
165
|
+
.slice(0, -1)
|
|
166
|
+
.join(".")}/${runtime}/${artifact}`, target: "_blank", className: "text-white", rel: "noopener noreferrer" }, artifact.split("/").pop()))))))))))),
|
|
167
|
+
given.thens.map((then, k) => (React.createElement("div", { key: `t-${k}`, className: `p-3 mb-2 ${then.error
|
|
168
|
+
? "bg-danger text-white"
|
|
169
|
+
: "bg-success text-white"}` },
|
|
170
|
+
React.createElement("div", { className: "d-flex justify-content-between align-items-start" },
|
|
171
|
+
React.createElement("div", null,
|
|
172
|
+
React.createElement("div", null,
|
|
173
|
+
React.createElement("strong", null, "Then:"),
|
|
174
|
+
" ",
|
|
175
|
+
then.name,
|
|
176
|
+
then.features && then.features.length > 0 && (React.createElement("div", { className: "mt-2" },
|
|
177
|
+
React.createElement("small", null, "Features:"),
|
|
178
|
+
React.createElement("ul", { className: "list-unstyled" }, then.features.map((feature, fi) => (React.createElement("li", { key: fi }, feature.startsWith("http") ? (React.createElement("a", { href: feature, target: "_blank", rel: "noopener noreferrer" }, new URL(feature).hostname)) : (feature))))))),
|
|
179
|
+
then.error && React.createElement("pre", { className: "mt-2" }, then.error))),
|
|
180
|
+
then.artifacts && then.artifacts.length > 0 && (React.createElement("div", { className: "ms-3" },
|
|
181
|
+
React.createElement("strong", null, "Artifacts:"),
|
|
182
|
+
React.createElement("ul", { className: "list-unstyled" }, then.artifacts.map((artifact, ai) => (React.createElement("li", { key: ai },
|
|
183
|
+
React.createElement("a", { href: `reports/${projectName}/${testName
|
|
184
|
+
.split(".")
|
|
185
|
+
.slice(0, -1)
|
|
186
|
+
.join(".")}/${runtime}/${artifact}`, target: "_blank", className: "text-white", rel: "noopener noreferrer" }, artifact.split("/").pop()))))))))))))))),
|
|
187
|
+
(buildErrors.errors.length > 0 || buildErrors.warnings.length > 0) && (React.createElement("div", { className: "mb-4 card border-danger" },
|
|
188
|
+
React.createElement("div", { className: "card-header bg-danger text-white" },
|
|
189
|
+
React.createElement("h4", null, "Build Errors and Warnings")),
|
|
190
|
+
React.createElement("div", { className: "card-body" },
|
|
191
|
+
buildErrors.errors.length > 0 && (React.createElement(React.Fragment, null,
|
|
192
|
+
React.createElement("h5", null, "Errors"),
|
|
193
|
+
React.createElement("ul", null, buildErrors.errors.map((error, idx) => (React.createElement("li", { key: `build-error-${idx}` },
|
|
194
|
+
React.createElement("strong", null, error.text),
|
|
195
|
+
error.location && (React.createElement("div", null,
|
|
196
|
+
"File: ",
|
|
197
|
+
error.location.file,
|
|
198
|
+
" Line: ",
|
|
199
|
+
error.location.line,
|
|
200
|
+
" Column: ",
|
|
201
|
+
error.location.column)))))))),
|
|
202
|
+
buildErrors.warnings.length > 0 && (React.createElement(React.Fragment, null,
|
|
203
|
+
React.createElement("h5", null, "Warnings"),
|
|
204
|
+
React.createElement("ul", null, buildErrors.warnings.map((warning, idx) => (React.createElement("li", { key: `build-warning-${idx}` },
|
|
205
|
+
React.createElement("strong", null, warning.text),
|
|
206
|
+
warning.location && (React.createElement("div", null,
|
|
207
|
+
"File: ",
|
|
208
|
+
warning.location.file,
|
|
209
|
+
" Line: ",
|
|
210
|
+
warning.location.line,
|
|
211
|
+
" Column: ",
|
|
212
|
+
warning.location.column)))))))))))));
|
|
151
213
|
};
|
|
152
214
|
console.log("Rendering TestPageView with logs:", {
|
|
153
215
|
logKeys: Object.keys(logs),
|
|
@@ -244,10 +306,10 @@ export const TestPageView = ({ projectName, testName, decodedTestPath, runtime,
|
|
|
244
306
|
React.createElement("i", { className: `bi bi-chevron-${expandedSections.standardLogs ? 'down' : 'right'} me-1` }),
|
|
245
307
|
React.createElement("span", null, "Standard Logs")),
|
|
246
308
|
expandedSections.standardLogs && (React.createElement("div", null, Object.values(STANDARD_LOGS).map((logName) => {
|
|
247
|
-
const logContent = logs[logName];
|
|
309
|
+
const logContent = logs ? logs[logName] : undefined;
|
|
248
310
|
const exists = logContent !== undefined &&
|
|
249
311
|
((typeof logContent === "string" && logContent.trim() !== "") ||
|
|
250
|
-
(typeof logContent === "object" && Object.keys(logContent).length > 0));
|
|
312
|
+
(typeof logContent === "object" && logContent !== null && Object.keys(logContent).length > 0));
|
|
251
313
|
return (React.createElement(FileTreeItem, { key: logName, name: logName, isFile: true, level: 1, isSelected: activeTab === logName, exists: exists, onClick: () => {
|
|
252
314
|
if (exists) {
|
|
253
315
|
setActiveTab(logName);
|
|
@@ -267,15 +329,16 @@ export const TestPageView = ({ projectName, testName, decodedTestPath, runtime,
|
|
|
267
329
|
}
|
|
268
330
|
} }));
|
|
269
331
|
})))),
|
|
270
|
-
|
|
332
|
+
runtime && RUNTIME_SPECIFIC_LOGS[runtime] &&
|
|
333
|
+
Object.values(RUNTIME_SPECIFIC_LOGS[runtime]).length > 0 && (React.createElement("div", { className: "p-2" },
|
|
271
334
|
React.createElement("div", { className: "d-flex align-items-center text-muted mb-1", style: { cursor: 'pointer', fontSize: '0.875rem' }, onClick: () => setExpandedSections(prev => (Object.assign(Object.assign({}, prev), { runtimeLogs: !prev.runtimeLogs }))) },
|
|
272
335
|
React.createElement("i", { className: `bi bi-chevron-${expandedSections.runtimeLogs ? 'down' : 'right'} me-1` }),
|
|
273
336
|
React.createElement("span", null, "Runtime Logs")),
|
|
274
337
|
expandedSections.runtimeLogs && (React.createElement("div", null, Object.values(RUNTIME_SPECIFIC_LOGS[runtime]).map((logName) => {
|
|
275
|
-
const logContent = logs[logName];
|
|
338
|
+
const logContent = logs ? logs[logName] : undefined;
|
|
276
339
|
const exists = logContent !== undefined &&
|
|
277
340
|
((typeof logContent === "string" && logContent.trim() !== "") ||
|
|
278
|
-
(typeof logContent === "object" && Object.keys(logContent).length > 0));
|
|
341
|
+
(typeof logContent === "object" && logContent !== null && Object.keys(logContent).length > 0));
|
|
279
342
|
return (React.createElement(FileTreeItem, { key: logName, name: logName, isFile: true, level: 1, isSelected: activeTab === logName, exists: exists, onClick: () => {
|
|
280
343
|
if (exists) {
|
|
281
344
|
setActiveTab(logName);
|
|
@@ -295,7 +358,7 @@ export const TestPageView = ({ projectName, testName, decodedTestPath, runtime,
|
|
|
295
358
|
}
|
|
296
359
|
} }));
|
|
297
360
|
}))))),
|
|
298
|
-
logs.source_files && (React.createElement("div", { className: "p-2" },
|
|
361
|
+
logs && logs.source_files && (React.createElement("div", { className: "p-2" },
|
|
299
362
|
React.createElement("div", { className: "d-flex align-items-center text-muted mb-1", style: { cursor: 'pointer', fontSize: '0.875rem' }, onClick: () => setExpandedSections(prev => (Object.assign(Object.assign({}, prev), { sourceFiles: !prev.sourceFiles }))) },
|
|
300
363
|
React.createElement("i", { className: `bi bi-chevron-${expandedSections.sourceFiles ? 'down' : 'right'} me-1` }),
|
|
301
364
|
React.createElement("span", null, "Source Files")),
|
|
@@ -326,7 +389,59 @@ export const TestPageView = ({ projectName, testName, decodedTestPath, runtime,
|
|
|
326
389
|
React.createElement("img", { src: selectedFile.content, alt: selectedFile.path, className: "img-fluid", style: { maxHeight: '300px' } }),
|
|
327
390
|
React.createElement("div", { className: "mt-2" },
|
|
328
391
|
React.createElement("a", { href: selectedFile.content, target: "_blank", rel: "noopener noreferrer", className: "btn btn-sm btn-outline-primary" }, "Open Full Size")))),
|
|
329
|
-
(selectedFile === null || selectedFile === void 0 ? void 0 : selectedFile.path.endsWith(".json")) &&
|
|
392
|
+
(selectedFile === null || selectedFile === void 0 ? void 0 : selectedFile.path.endsWith("build.json")) && (React.createElement("div", null,
|
|
393
|
+
React.createElement("h5", null, "Build Information"),
|
|
394
|
+
(() => {
|
|
395
|
+
var _a, _b;
|
|
396
|
+
try {
|
|
397
|
+
const buildData = JSON.parse(selectedFile.content);
|
|
398
|
+
return (React.createElement(React.Fragment, null,
|
|
399
|
+
((_a = buildData.errors) === null || _a === void 0 ? void 0 : _a.length) > 0 && (React.createElement("div", { className: "mb-3" },
|
|
400
|
+
React.createElement("h6", { className: "text-danger" },
|
|
401
|
+
"Errors (",
|
|
402
|
+
buildData.errors.length,
|
|
403
|
+
")"),
|
|
404
|
+
React.createElement("ul", { className: "list-unstyled" }, buildData.errors.map((error, index) => (React.createElement("li", { key: index, className: "mb-2 p-2 bg-light rounded" },
|
|
405
|
+
React.createElement("div", { className: "text-danger fw-bold" }, error.text),
|
|
406
|
+
error.location && (React.createElement("div", { className: "small text-muted" },
|
|
407
|
+
"File: ",
|
|
408
|
+
error.location.file,
|
|
409
|
+
"Line: ",
|
|
410
|
+
error.location.line,
|
|
411
|
+
"Column: ",
|
|
412
|
+
error.location.column)),
|
|
413
|
+
error.notes && error.notes.length > 0 && (React.createElement("div", { className: "small" },
|
|
414
|
+
"Notes:",
|
|
415
|
+
React.createElement("ul", null, error.notes.map((note, noteIndex) => (React.createElement("li", { key: noteIndex }, note.text)))))))))))),
|
|
416
|
+
((_b = buildData.warnings) === null || _b === void 0 ? void 0 : _b.length) > 0 && (React.createElement("div", { className: "mb-3" },
|
|
417
|
+
React.createElement("h6", { className: "text-warning" },
|
|
418
|
+
"Warnings (",
|
|
419
|
+
buildData.warnings.length,
|
|
420
|
+
")"),
|
|
421
|
+
React.createElement("ul", { className: "list-unstyled" }, buildData.warnings.map((warning, index) => (React.createElement("li", { key: index, className: "mb-2 p-2 bg-light rounded" },
|
|
422
|
+
React.createElement("div", { className: "text-warning fw-bold" }, warning.text),
|
|
423
|
+
warning.location && (React.createElement("div", { className: "small text-muted" },
|
|
424
|
+
"File: ",
|
|
425
|
+
warning.location.file,
|
|
426
|
+
"Line: ",
|
|
427
|
+
warning.location.line,
|
|
428
|
+
"Column: ",
|
|
429
|
+
warning.location.column)),
|
|
430
|
+
warning.notes && warning.notes.length > 0 && (React.createElement("div", { className: "small" },
|
|
431
|
+
"Notes:",
|
|
432
|
+
React.createElement("ul", null, warning.notes.map((note, noteIndex) => (React.createElement("li", { key: noteIndex }, note.text)))))))))))),
|
|
433
|
+
(!buildData.errors || buildData.errors.length === 0) &&
|
|
434
|
+
(!buildData.warnings || buildData.warnings.length === 0) && (React.createElement("div", { className: "alert alert-success" }, "No build errors or warnings"))));
|
|
435
|
+
}
|
|
436
|
+
catch (e) {
|
|
437
|
+
return (React.createElement("div", { className: "alert alert-danger" },
|
|
438
|
+
"Error parsing build.json: ",
|
|
439
|
+
e.message));
|
|
440
|
+
}
|
|
441
|
+
})())),
|
|
442
|
+
(selectedFile === null || selectedFile === void 0 ? void 0 : selectedFile.path.endsWith(".json")) &&
|
|
443
|
+
!selectedFile.path.endsWith("tests.json") &&
|
|
444
|
+
!selectedFile.path.endsWith("build.json") && (React.createElement("pre", { className: "bg-light p-2 small" },
|
|
330
445
|
React.createElement("code", null, selectedFile.content))),
|
|
331
446
|
(selectedFile === null || selectedFile === void 0 ? void 0 : selectedFile.path.includes("source_files")) && (React.createElement("div", null,
|
|
332
447
|
React.createElement("div", { className: "mb-2 small text-muted" },
|