mindoodb-app-sdk 0.0.53 → 0.0.55
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/README.md +120 -55
- package/TESTING.md +36 -2
- package/dist/{chunk-6NTVTL6G.js → chunk-S73KTYZF.js} +139 -6
- package/dist/index.d.ts +80 -3
- package/dist/index.js +276 -8
- package/dist/testing/index.d.ts +170 -10
- package/dist/testing/index.js +731 -62
- package/dist/{types-CsKw_niD.d.ts → types-Dl70Tlfj.d.ts} +144 -2
- package/dist/vite/index.d.ts +18 -5
- package/dist/vite/index.js +156 -2
- package/package.json +1 -1
package/dist/testing/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MINDOODB_APP_VALUE_TAG,
|
|
3
|
+
pageUsers,
|
|
3
4
|
releaseMindooDBAppBridgeSessions
|
|
4
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-S73KTYZF.js";
|
|
5
6
|
|
|
6
7
|
// src/testing/index.ts
|
|
7
8
|
import * as Automerge from "@automerge/automerge";
|
|
@@ -454,7 +455,202 @@ async function createEvaluatingViewNavigator(input, documents) {
|
|
|
454
455
|
return wrapNavigator(input.definition, navigator);
|
|
455
456
|
}
|
|
456
457
|
|
|
458
|
+
// src/testing/capabilityGuard.ts
|
|
459
|
+
var NONE = [];
|
|
460
|
+
var READ = ["read"];
|
|
461
|
+
var DOCUMENT_REQUIREMENTS = {
|
|
462
|
+
// capability probes answer false instead of failing
|
|
463
|
+
canChange: NONE,
|
|
464
|
+
canCreate: NONE,
|
|
465
|
+
canDelete: NONE,
|
|
466
|
+
canUndelete: NONE,
|
|
467
|
+
create: ["create"],
|
|
468
|
+
createMany: ["create"],
|
|
469
|
+
getDefaultCreateKeyId: ["create"],
|
|
470
|
+
listCreateKeys: ["create"],
|
|
471
|
+
update: ["update"],
|
|
472
|
+
applyAutomergeChanges: ["update"],
|
|
473
|
+
applyAutomergeChangesBatch: ["update"],
|
|
474
|
+
addRecipients: ["update"],
|
|
475
|
+
removeRecipients: ["update"],
|
|
476
|
+
setRecipients: ["update"],
|
|
477
|
+
delete: ["delete"],
|
|
478
|
+
deleteMany: ["delete"],
|
|
479
|
+
undelete: ["delete"],
|
|
480
|
+
listHistory: ["history"],
|
|
481
|
+
getAtHeads: ["history"],
|
|
482
|
+
getAtRevision: ["history"],
|
|
483
|
+
getAtTimestamp: ["history"],
|
|
484
|
+
listVerification: ["history"]
|
|
485
|
+
};
|
|
486
|
+
var ATTACHMENT_REQUIREMENTS = {
|
|
487
|
+
openWriteStream: ["attachments", "update"],
|
|
488
|
+
remove: ["attachments", "update"],
|
|
489
|
+
scan: ["attachments", "update"]
|
|
490
|
+
};
|
|
491
|
+
var DATABASE_REQUIREMENTS = {
|
|
492
|
+
info: NONE,
|
|
493
|
+
getFulltextSetup: READ,
|
|
494
|
+
getExtractionSetup: READ,
|
|
495
|
+
getSummarySetup: READ,
|
|
496
|
+
setFulltextSetup: ["update"],
|
|
497
|
+
setExtractionSetup: ["update"],
|
|
498
|
+
setSummarySetup: ["update"]
|
|
499
|
+
};
|
|
500
|
+
var MockForbiddenError = class extends Error {
|
|
501
|
+
name = "forbidden";
|
|
502
|
+
};
|
|
503
|
+
function guardApi(api, requirementOf, check) {
|
|
504
|
+
const guarded = {};
|
|
505
|
+
for (const [key, value] of Object.entries(api)) {
|
|
506
|
+
guarded[key] = typeof value === "function" ? (...args) => {
|
|
507
|
+
try {
|
|
508
|
+
check(key, requirementOf(key));
|
|
509
|
+
} catch (error) {
|
|
510
|
+
return Promise.reject(error);
|
|
511
|
+
}
|
|
512
|
+
return value.apply(api, args);
|
|
513
|
+
} : value;
|
|
514
|
+
}
|
|
515
|
+
return guarded;
|
|
516
|
+
}
|
|
517
|
+
function guardDatabase(database, currentInfo, enforce) {
|
|
518
|
+
const check = (area) => (method, requirement) => {
|
|
519
|
+
if (!enforce()) return;
|
|
520
|
+
const info = currentInfo();
|
|
521
|
+
for (const capability of requirement) {
|
|
522
|
+
if (!info.capabilities.includes(capability)) {
|
|
523
|
+
throw new MockForbiddenError(
|
|
524
|
+
`Database "${info.id}" was not granted the "${capability}" capability (${area}${method})`
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
const predict = (fn, capability) => async (...args) => {
|
|
530
|
+
const info = currentInfo();
|
|
531
|
+
if (enforce() && !info.capabilities.includes(capability)) {
|
|
532
|
+
return {
|
|
533
|
+
allowed: false,
|
|
534
|
+
reason: `database "${info.id}" was not granted the "${capability}" capability`,
|
|
535
|
+
tier: "tier1"
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
return await fn.apply(database.documents, args);
|
|
539
|
+
};
|
|
540
|
+
const base = guardApi(
|
|
541
|
+
database,
|
|
542
|
+
(method) => DATABASE_REQUIREMENTS[method] ?? READ,
|
|
543
|
+
check("")
|
|
544
|
+
);
|
|
545
|
+
return {
|
|
546
|
+
...base,
|
|
547
|
+
async info() {
|
|
548
|
+
const info = currentInfo();
|
|
549
|
+
return { ...info, capabilities: [...info.capabilities] };
|
|
550
|
+
},
|
|
551
|
+
documents: {
|
|
552
|
+
...guardApi(
|
|
553
|
+
database.documents,
|
|
554
|
+
(method) => DOCUMENT_REQUIREMENTS[method] ?? READ,
|
|
555
|
+
check("documents.")
|
|
556
|
+
),
|
|
557
|
+
// the non-throwing predictions report the missing capability instead
|
|
558
|
+
canCreate: predict(database.documents.canCreate, "create"),
|
|
559
|
+
canChange: predict(database.documents.canChange, "update"),
|
|
560
|
+
canDelete: predict(database.documents.canDelete, "delete"),
|
|
561
|
+
canUndelete: predict(database.documents.canUndelete, "delete")
|
|
562
|
+
},
|
|
563
|
+
attachments: guardApi(
|
|
564
|
+
database.attachments,
|
|
565
|
+
(method) => ATTACHMENT_REQUIREMENTS[method] ?? ["attachments"],
|
|
566
|
+
check("attachments.")
|
|
567
|
+
),
|
|
568
|
+
identity: guardApi(database.identity, () => ["sign"], check("identity.")),
|
|
569
|
+
timestamps: guardApi(database.timestamps, () => ["timestamps"], check("timestamps.")),
|
|
570
|
+
directory: guardApi(database.directory, () => ["directory"], check("directory."))
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/testing/memoryAttachments.ts
|
|
575
|
+
function concat(chunks) {
|
|
576
|
+
const bytes = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0));
|
|
577
|
+
let offset = 0;
|
|
578
|
+
for (const chunk of chunks) {
|
|
579
|
+
bytes.set(chunk, offset);
|
|
580
|
+
offset += chunk.length;
|
|
581
|
+
}
|
|
582
|
+
return bytes;
|
|
583
|
+
}
|
|
584
|
+
function createMemoryAttachments() {
|
|
585
|
+
const files = /* @__PURE__ */ new Map();
|
|
586
|
+
const forDoc = (docId) => {
|
|
587
|
+
let map = files.get(docId);
|
|
588
|
+
if (!map) files.set(docId, map = /* @__PURE__ */ new Map());
|
|
589
|
+
return map;
|
|
590
|
+
};
|
|
591
|
+
return {
|
|
592
|
+
async list(docId) {
|
|
593
|
+
return [...forDoc(docId)].map(
|
|
594
|
+
([fileName, file]) => ({
|
|
595
|
+
attachmentId: `${docId}/${fileName}`,
|
|
596
|
+
fileName,
|
|
597
|
+
mimeType: file.mimeType,
|
|
598
|
+
size: file.bytes.length
|
|
599
|
+
})
|
|
600
|
+
);
|
|
601
|
+
},
|
|
602
|
+
async remove(docId, attachmentName) {
|
|
603
|
+
forDoc(docId).delete(attachmentName);
|
|
604
|
+
return { ok: true };
|
|
605
|
+
},
|
|
606
|
+
async openReadStream(docId, attachmentName) {
|
|
607
|
+
const file = forDoc(docId).get(attachmentName);
|
|
608
|
+
if (!file) throw new Error(`Attachment not found: ${attachmentName}`);
|
|
609
|
+
let done = false;
|
|
610
|
+
return {
|
|
611
|
+
async read() {
|
|
612
|
+
if (done) return null;
|
|
613
|
+
done = true;
|
|
614
|
+
return file.bytes.slice();
|
|
615
|
+
},
|
|
616
|
+
async close() {
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
},
|
|
620
|
+
async openWriteStream(docId, attachmentName, contentType) {
|
|
621
|
+
const chunks = [];
|
|
622
|
+
return {
|
|
623
|
+
async write(chunk) {
|
|
624
|
+
chunks.push(new Uint8Array(chunk));
|
|
625
|
+
},
|
|
626
|
+
async close() {
|
|
627
|
+
forDoc(docId).set(attachmentName, {
|
|
628
|
+
bytes: concat(chunks),
|
|
629
|
+
mimeType: contentType ?? "application/octet-stream"
|
|
630
|
+
});
|
|
631
|
+
},
|
|
632
|
+
async abort() {
|
|
633
|
+
chunks.length = 0;
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
457
640
|
// src/testing/browserTestHost.ts
|
|
641
|
+
var ALL_CAPABILITIES = [
|
|
642
|
+
"read",
|
|
643
|
+
"create",
|
|
644
|
+
"update",
|
|
645
|
+
"delete",
|
|
646
|
+
"history",
|
|
647
|
+
"attachments",
|
|
648
|
+
"views",
|
|
649
|
+
"directory",
|
|
650
|
+
"sign",
|
|
651
|
+
"timestamps",
|
|
652
|
+
"sealedchannel"
|
|
653
|
+
];
|
|
458
654
|
function capabilitiesForDefinitionPermissions(permissions) {
|
|
459
655
|
const result = /* @__PURE__ */ new Set(["read"]);
|
|
460
656
|
for (const permission of permissions ?? []) {
|
|
@@ -467,32 +663,104 @@ function capabilitiesForDefinitionPermissions(permissions) {
|
|
|
467
663
|
}
|
|
468
664
|
return [...result];
|
|
469
665
|
}
|
|
470
|
-
function mockDatabasesFromDefinition(definition, seed = {}) {
|
|
471
|
-
return (definition.databases ?? []).map((database) =>
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
666
|
+
function mockDatabasesFromDefinition(definition, seed = {}, options = {}) {
|
|
667
|
+
return (definition.databases ?? []).filter((database) => options.overrides?.[database.logicalDatabaseId]?.mapped !== false).map((database) => {
|
|
668
|
+
const override = options.overrides?.[database.logicalDatabaseId];
|
|
669
|
+
return {
|
|
670
|
+
info: {
|
|
671
|
+
id: database.logicalDatabaseId,
|
|
672
|
+
title: override?.title ?? database.label ?? database.logicalDatabaseId,
|
|
673
|
+
capabilities: override?.capabilities ? [...new Set(override.capabilities)] : capabilitiesForDefinitionPermissions(database.permissions)
|
|
674
|
+
},
|
|
675
|
+
documents: seed[database.logicalDatabaseId],
|
|
676
|
+
...options.attachments === "none" ? {} : { methods: { attachments: createMemoryAttachments() } }
|
|
677
|
+
};
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
function readTestHostUrlSettings(search) {
|
|
681
|
+
const params = new URLSearchParams(search);
|
|
682
|
+
const settings = { overrides: {} };
|
|
683
|
+
for (const entry of params.getAll("db")) {
|
|
684
|
+
const separator = entry.indexOf(":");
|
|
685
|
+
if (separator <= 0) continue;
|
|
686
|
+
const id = entry.slice(0, separator);
|
|
687
|
+
const list = entry.slice(separator + 1);
|
|
688
|
+
settings.overrides[id] = list === "none" ? { mapped: false } : {
|
|
689
|
+
capabilities: list.split(",").map((capability) => capability.trim()).filter(
|
|
690
|
+
(capability) => ALL_CAPABILITIES.includes(capability)
|
|
691
|
+
)
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const enforce = params.get("enforce");
|
|
695
|
+
if (enforce !== null) settings.enforceCapabilities = enforce === "1" || enforce === "true";
|
|
696
|
+
const users = Number.parseInt(params.get("users") ?? "", 10);
|
|
697
|
+
if (Number.isFinite(users) && users >= 0) settings.directoryUserCount = users;
|
|
698
|
+
const locale = params.get("locale");
|
|
699
|
+
if (locale) settings.locale = locale;
|
|
700
|
+
const theme = params.get("theme");
|
|
701
|
+
if (theme === "dark" || theme === "light") settings.theme = theme;
|
|
702
|
+
return settings;
|
|
479
703
|
}
|
|
480
704
|
function withLaunchId(appUrl, launchId) {
|
|
481
705
|
const url = new URL(appUrl, window.location.href);
|
|
482
706
|
url.searchParams.set("mindoodbAppLaunchId", launchId);
|
|
483
707
|
return url.toString();
|
|
484
708
|
}
|
|
709
|
+
var TINY_PNG = Uint8Array.from(
|
|
710
|
+
atob(
|
|
711
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN4+O7dfwAJYgPUBHcDcgAAAABJRU5ErkJggg=="
|
|
712
|
+
),
|
|
713
|
+
(char) => char.charCodeAt(0)
|
|
714
|
+
);
|
|
715
|
+
async function sampleScan() {
|
|
716
|
+
const canvas = document.createElement("canvas");
|
|
717
|
+
canvas.width = 1200;
|
|
718
|
+
canvas.height = 900;
|
|
719
|
+
const g = typeof canvas.toBlob === "function" ? canvas.getContext("2d") : null;
|
|
720
|
+
if (!g) {
|
|
721
|
+
return { fileName: "sample-scan.png", mimeType: "image/png", bytes: TINY_PNG };
|
|
722
|
+
}
|
|
723
|
+
g.fillStyle = "#f4f1ea";
|
|
724
|
+
g.fillRect(0, 0, canvas.width, canvas.height);
|
|
725
|
+
g.fillStyle = "#1f2a44";
|
|
726
|
+
g.font = "bold 64px sans-serif";
|
|
727
|
+
g.fillText("Scanned page", 90, 150);
|
|
728
|
+
g.fillStyle = "#8a93a6";
|
|
729
|
+
for (let line = 0; line < 9; line += 1) {
|
|
730
|
+
g.fillRect(90, 240 + line * 60, line % 3 === 2 ? 620 : 1020, 18);
|
|
731
|
+
}
|
|
732
|
+
g.strokeStyle = "#c0392b";
|
|
733
|
+
g.lineWidth = 8;
|
|
734
|
+
g.beginPath();
|
|
735
|
+
g.arc(960, 740, 110, 0, Math.PI * 2);
|
|
736
|
+
g.stroke();
|
|
737
|
+
g.fillStyle = "#c0392b";
|
|
738
|
+
g.font = "bold 40px sans-serif";
|
|
739
|
+
g.fillText("TEST", 910, 755);
|
|
740
|
+
const blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/jpeg", 0.9));
|
|
741
|
+
if (!blob) {
|
|
742
|
+
return { fileName: "sample-scan.png", mimeType: "image/png", bytes: TINY_PNG };
|
|
743
|
+
}
|
|
744
|
+
return {
|
|
745
|
+
fileName: "sample-scan.jpg",
|
|
746
|
+
mimeType: "image/jpeg",
|
|
747
|
+
bytes: new Uint8Array(await blob.arrayBuffer())
|
|
748
|
+
};
|
|
749
|
+
}
|
|
485
750
|
function createBrowserTestHost(options) {
|
|
486
|
-
const { frame, appUrl, onChange, ...hostOptions } = options;
|
|
487
|
-
let nextScan
|
|
751
|
+
const { frame, appUrl, onChange, scanMode: initialScanMode, ...hostOptions } = options;
|
|
752
|
+
let nextScan;
|
|
753
|
+
let scanMode = initialScanMode ?? "cancel";
|
|
488
754
|
let scanCounter = 0;
|
|
489
755
|
const log = {
|
|
490
756
|
notifications: [],
|
|
491
757
|
previews: [],
|
|
492
758
|
scans: [],
|
|
493
759
|
focusRequests: 0,
|
|
494
|
-
requests: []
|
|
760
|
+
requests: [],
|
|
761
|
+
responses: {}
|
|
495
762
|
};
|
|
763
|
+
const changed = () => queueMicrotask(() => onChange?.());
|
|
496
764
|
const host = createFakeBridgeHost({
|
|
497
765
|
...hostOptions,
|
|
498
766
|
onRequest(request) {
|
|
@@ -515,29 +783,56 @@ function createBrowserTestHost(options) {
|
|
|
515
783
|
});
|
|
516
784
|
}
|
|
517
785
|
hostOptions.onRequest?.(request);
|
|
518
|
-
|
|
786
|
+
changed();
|
|
787
|
+
},
|
|
788
|
+
onResponse(request, response) {
|
|
789
|
+
log.responses[request.id] = response;
|
|
790
|
+
hostOptions.onResponse?.(request, response);
|
|
791
|
+
changed();
|
|
519
792
|
},
|
|
520
793
|
requestHandlers: {
|
|
521
|
-
"attachments.scanAndWrite": (params) => {
|
|
794
|
+
"attachments.scanAndWrite": async (params, { host: controller }) => {
|
|
522
795
|
const input = params ?? {};
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
796
|
+
const databaseId = String(input.databaseId);
|
|
797
|
+
const docId = String(input.docId);
|
|
798
|
+
if (controller.getEnforceCapabilities()) {
|
|
799
|
+
const info = controller.listDatabases().find((entry) => entry.id === databaseId);
|
|
800
|
+
const missing = ["attachments", "update"].find(
|
|
801
|
+
(capability) => !info?.capabilities.includes(capability)
|
|
802
|
+
);
|
|
803
|
+
if (missing) {
|
|
804
|
+
const error = new Error(
|
|
805
|
+
`Database "${databaseId}" was not granted the "${missing}" capability (attachments.scan)`
|
|
806
|
+
);
|
|
807
|
+
error.name = "forbidden";
|
|
808
|
+
throw error;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
const pending = nextScan;
|
|
812
|
+
nextScan = void 0;
|
|
813
|
+
const mode = pending === void 0 ? scanMode : pending;
|
|
814
|
+
const scan = mode === null || mode === "cancel" ? null : mode === "sample" ? await sampleScan() : mode;
|
|
530
815
|
if (!scan) {
|
|
816
|
+
log.scans.push({ databaseId, docId, result: "cancelled" });
|
|
531
817
|
return { ok: false };
|
|
532
818
|
}
|
|
533
819
|
scanCounter += 1;
|
|
820
|
+
const fileName = mode === "sample" && typeof input.defaultFileName === "string" ? input.defaultFileName : scan.fileName;
|
|
821
|
+
if (scan.bytes) {
|
|
822
|
+
const database = await controller.session.openDatabase(databaseId);
|
|
823
|
+
const writer = await database.attachments.openWriteStream(docId, fileName, scan.mimeType);
|
|
824
|
+
await writer.write(scan.bytes);
|
|
825
|
+
await writer.close();
|
|
826
|
+
}
|
|
827
|
+
const size = scan.bytes?.length ?? scan.size ?? 0;
|
|
828
|
+
log.scans.push({ databaseId, docId, result: "scanned", fileName, mimeType: scan.mimeType, size });
|
|
534
829
|
return {
|
|
535
830
|
ok: true,
|
|
536
831
|
attachment: {
|
|
537
832
|
attachmentId: `test-scan-${scanCounter}`,
|
|
538
|
-
fileName
|
|
833
|
+
fileName,
|
|
539
834
|
mimeType: scan.mimeType,
|
|
540
|
-
size
|
|
835
|
+
size
|
|
541
836
|
}
|
|
542
837
|
};
|
|
543
838
|
},
|
|
@@ -552,12 +847,19 @@ function createBrowserTestHost(options) {
|
|
|
552
847
|
};
|
|
553
848
|
window.addEventListener("message", onMessage);
|
|
554
849
|
frame.src = withLaunchId(appUrl, host.launchId);
|
|
850
|
+
const reloadApp = () => {
|
|
851
|
+
frame.src = withLaunchId(appUrl, host.launchId);
|
|
852
|
+
};
|
|
555
853
|
return {
|
|
556
854
|
host,
|
|
557
855
|
log,
|
|
558
856
|
setNextScan(scan) {
|
|
559
857
|
nextScan = scan;
|
|
560
858
|
},
|
|
859
|
+
setScanMode(mode) {
|
|
860
|
+
scanMode = mode;
|
|
861
|
+
},
|
|
862
|
+
getScanMode: () => scanMode,
|
|
561
863
|
setTheme(theme) {
|
|
562
864
|
host.emitThemeChange(theme);
|
|
563
865
|
onChange?.();
|
|
@@ -566,37 +868,132 @@ function createBrowserTestHost(options) {
|
|
|
566
868
|
host.emitHostFocusChange(focused);
|
|
567
869
|
onChange?.();
|
|
568
870
|
},
|
|
569
|
-
|
|
570
|
-
|
|
871
|
+
setLocale(locale) {
|
|
872
|
+
host.emitLocaleChange(locale);
|
|
873
|
+
onChange?.();
|
|
571
874
|
},
|
|
875
|
+
setCapabilities(databaseId, capabilities) {
|
|
876
|
+
host.setCapabilities(databaseId, capabilities);
|
|
877
|
+
reloadApp();
|
|
878
|
+
onChange?.();
|
|
879
|
+
},
|
|
880
|
+
setEnforceCapabilities(enforce) {
|
|
881
|
+
host.setEnforceCapabilities(enforce);
|
|
882
|
+
onChange?.();
|
|
883
|
+
},
|
|
884
|
+
setDirectoryUsers(users) {
|
|
885
|
+
host.setDirectoryUsers(users);
|
|
886
|
+
onChange?.();
|
|
887
|
+
},
|
|
888
|
+
reloadApp,
|
|
572
889
|
dispose() {
|
|
573
890
|
window.removeEventListener("message", onMessage);
|
|
574
891
|
host.dispose();
|
|
575
892
|
}
|
|
576
893
|
};
|
|
577
894
|
}
|
|
895
|
+
var TEST_HOST_LOCALES = [
|
|
896
|
+
{ tag: "en-US", label: "English" },
|
|
897
|
+
{ tag: "de-DE", label: "Deutsch" },
|
|
898
|
+
{ tag: "fr-FR", label: "Fran\xE7ais" },
|
|
899
|
+
{ tag: "es-ES", label: "Espa\xF1ol" },
|
|
900
|
+
{ tag: "it-IT", label: "Italiano" },
|
|
901
|
+
{ tag: "pt-BR", label: "Portugu\xEAs" },
|
|
902
|
+
{ tag: "nl-NL", label: "Nederlands" },
|
|
903
|
+
{ tag: "pl-PL", label: "Polski" },
|
|
904
|
+
{ tag: "cs-CZ", label: "\u010Ce\u0161tina" },
|
|
905
|
+
{ tag: "ru-RU", label: "\u0420\u0443\u0441\u0441\u043A\u0438\u0439" },
|
|
906
|
+
{ tag: "zh-CN", label: "\u7B80\u4F53\u4E2D\u6587" },
|
|
907
|
+
{ tag: "zh-TW", label: "\u7E41\u9AD4\u4E2D\u6587" },
|
|
908
|
+
{ tag: "ja-JP", label: "\u65E5\u672C\u8A9E" },
|
|
909
|
+
{ tag: "ko-KR", label: "\uD55C\uAD6D\uC5B4" },
|
|
910
|
+
{ tag: "th-TH", label: "\u0E44\u0E17\u0E22" },
|
|
911
|
+
{ tag: "vi-VN", label: "Ti\u1EBFng Vi\u1EC7t" },
|
|
912
|
+
{ tag: "id-ID", label: "Bahasa Indonesia" },
|
|
913
|
+
{ tag: "ms-MY", label: "Bahasa Melayu" },
|
|
914
|
+
{ tag: "hi-IN", label: "\u0939\u093F\u0928\u094D\u0926\u0940" },
|
|
915
|
+
{ tag: "ar-SA", label: "\u0627\u0644\u0639\u0631\u0628\u064A\u0629" },
|
|
916
|
+
{ tag: "he-IL", label: "\u05E2\u05D1\u05E8\u05D9\u05EA" }
|
|
917
|
+
];
|
|
918
|
+
var DIRECTORY_SIZES = [0, 5, 25, 120, 500];
|
|
578
919
|
var STYLE = `
|
|
579
|
-
.htest{display:grid;grid-template-columns:minmax(0,1fr)
|
|
920
|
+
.htest{display:grid;grid-template-columns:minmax(0,1fr) 320px;height:100vh;margin:0;font:13px/1.45 system-ui,sans-serif;background:#eef1f7;color:#172033}
|
|
580
921
|
.htest *{box-sizing:border-box}
|
|
581
922
|
.htest__frame{width:100%;height:100%;border:0;background:#fff}
|
|
582
923
|
.htest__panel{border-left:1px solid #d7dce8;overflow:auto;padding:14px;background:#f8f9fc}
|
|
583
924
|
.htest__badge{display:inline-block;padding:2px 8px;border-radius:999px;background:#fde68a;color:#713f12;font-weight:600;font-size:11px;letter-spacing:.04em}
|
|
584
925
|
.htest h1{font-size:15px;margin:8px 0 12px}
|
|
585
926
|
.htest h2{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#5b6478;margin:16px 0 6px}
|
|
586
|
-
.
|
|
587
|
-
.
|
|
927
|
+
.htest h3{font-size:12px;margin:10px 0 4px}
|
|
928
|
+
.htest__row{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
|
|
929
|
+
.htest button,.htest select{font:inherit;padding:5px 9px;border-radius:7px;border:1px solid #c9d0e0;background:#fff;cursor:pointer;max-width:100%}
|
|
588
930
|
.htest ul{list-style:none;margin:0;padding:0}
|
|
589
931
|
.htest li{padding:5px 0;border-bottom:1px solid #e4e8f1;word-break:break-word}
|
|
590
932
|
.htest__muted{color:#5b6478}
|
|
933
|
+
.htest__caps{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:2px 8px}
|
|
934
|
+
.htest__caps label,.htest__check{display:flex;gap:5px;align-items:center}
|
|
935
|
+
.htest details summary{cursor:pointer;display:flex;gap:6px;align-items:baseline}
|
|
936
|
+
.htest details summary::-webkit-details-marker{display:none}
|
|
937
|
+
.htest details summary::before{content:"\u25B8";color:#8a93a6}
|
|
938
|
+
.htest details[open] summary::before{content:"\u25BE"}
|
|
939
|
+
.htest__meta{margin-left:auto;color:#5b6478;font-size:11px;white-space:nowrap}
|
|
940
|
+
.htest__error{color:#b42318}
|
|
941
|
+
.htest pre{margin:4px 0 2px;padding:6px 8px;background:#fff;border:1px solid #e4e8f1;border-radius:6px;font:11px/1.4 ui-monospace,monospace;white-space:pre-wrap;word-break:break-all;max-height:260px;overflow:auto}
|
|
591
942
|
@media (max-width:720px){.htest{grid-template-columns:1fr;grid-template-rows:65vh auto}.htest__panel{border-left:0;border-top:1px solid #d7dce8}}
|
|
592
943
|
`;
|
|
944
|
+
function previewJson(value, maxString = 400, maxItems = 30, maxDepth = 6) {
|
|
945
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
946
|
+
const walk = (entry, depth) => {
|
|
947
|
+
if (entry instanceof Uint8Array || entry instanceof ArrayBuffer) {
|
|
948
|
+
const bytes = entry instanceof ArrayBuffer ? new Uint8Array(entry) : entry;
|
|
949
|
+
const hex = Array.from(bytes.subarray(0, 16), (byte) => byte.toString(16).padStart(2, "0")).join(" ");
|
|
950
|
+
return `<${bytes.length} bytes${bytes.length ? `: ${hex}${bytes.length > 16 ? " \u2026" : ""}` : ""}>`;
|
|
951
|
+
}
|
|
952
|
+
if (typeof entry === "string") {
|
|
953
|
+
return entry.length > maxString ? `${entry.slice(0, maxString)}\u2026 (${entry.length} chars)` : entry;
|
|
954
|
+
}
|
|
955
|
+
if (entry === null || typeof entry !== "object") return entry;
|
|
956
|
+
if (seen.has(entry)) return "<circular>";
|
|
957
|
+
if (depth >= maxDepth) return Array.isArray(entry) ? `<array(${entry.length})>` : "<object>";
|
|
958
|
+
seen.add(entry);
|
|
959
|
+
if (Array.isArray(entry)) {
|
|
960
|
+
const items = entry.slice(0, maxItems).map((child) => walk(child, depth + 1));
|
|
961
|
+
if (entry.length > maxItems) items.push(`\u2026 ${entry.length - maxItems} more`);
|
|
962
|
+
return items;
|
|
963
|
+
}
|
|
964
|
+
const result = {};
|
|
965
|
+
for (const [key, child] of Object.entries(entry)) result[key] = walk(child, depth + 1);
|
|
966
|
+
return result;
|
|
967
|
+
};
|
|
968
|
+
try {
|
|
969
|
+
return JSON.stringify(walk(value, 0), null, 2) ?? "undefined";
|
|
970
|
+
} catch {
|
|
971
|
+
return String(value);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
593
974
|
function item(text) {
|
|
594
975
|
const li = document.createElement("li");
|
|
595
976
|
li.textContent = text;
|
|
596
977
|
return li;
|
|
597
978
|
}
|
|
598
979
|
function mountHavenTestHost(options) {
|
|
599
|
-
const {
|
|
980
|
+
const {
|
|
981
|
+
container = document.body,
|
|
982
|
+
title = "Haven test host",
|
|
983
|
+
locales = TEST_HOST_LOCALES,
|
|
984
|
+
urlSettings: useUrl = true,
|
|
985
|
+
...hostOptions
|
|
986
|
+
} = options;
|
|
987
|
+
const url = useUrl ? readTestHostUrlSettings(window.location.search) : { overrides: {} };
|
|
988
|
+
const databases = (hostOptions.databases ?? []).filter((database) => url.overrides[database.info.id]?.mapped !== false).map((database) => {
|
|
989
|
+
const capabilities = url.overrides[database.info.id]?.capabilities;
|
|
990
|
+
return capabilities ? { ...database, info: { ...database.info, capabilities } } : database;
|
|
991
|
+
});
|
|
992
|
+
const launchContext = {
|
|
993
|
+
...hostOptions.launchContext,
|
|
994
|
+
...url.locale ? { locale: url.locale } : {},
|
|
995
|
+
...url.theme ? { theme: { mode: url.theme, preset: "mindoo" } } : {}
|
|
996
|
+
};
|
|
600
997
|
const style = document.createElement("style");
|
|
601
998
|
style.textContent = STYLE;
|
|
602
999
|
document.head.append(style);
|
|
@@ -614,7 +1011,9 @@ function mountHavenTestHost(options) {
|
|
|
614
1011
|
document.body.style.margin = "0";
|
|
615
1012
|
}
|
|
616
1013
|
let focused = false;
|
|
617
|
-
let dark =
|
|
1014
|
+
let dark = url.theme === "dark";
|
|
1015
|
+
let initialDark = url.theme ? void 0 : false;
|
|
1016
|
+
const openRequests = /* @__PURE__ */ new Set();
|
|
618
1017
|
const lists = {
|
|
619
1018
|
notifications: document.createElement("ul"),
|
|
620
1019
|
previews: document.createElement("ul"),
|
|
@@ -630,11 +1029,106 @@ function mountHavenTestHost(options) {
|
|
|
630
1029
|
node.addEventListener("click", onClick);
|
|
631
1030
|
return node;
|
|
632
1031
|
}
|
|
1032
|
+
function hint(text) {
|
|
1033
|
+
const node = document.createElement("p");
|
|
1034
|
+
node.className = "htest__muted";
|
|
1035
|
+
node.textContent = text;
|
|
1036
|
+
return node;
|
|
1037
|
+
}
|
|
633
1038
|
function section(label, ...children) {
|
|
634
1039
|
const heading2 = document.createElement("h2");
|
|
635
1040
|
heading2.textContent = label;
|
|
636
1041
|
panel.append(heading2, ...children);
|
|
637
1042
|
}
|
|
1043
|
+
function syncUrl() {
|
|
1044
|
+
if (!useUrl) return;
|
|
1045
|
+
const params = new URLSearchParams(window.location.search);
|
|
1046
|
+
for (const key of ["db", "enforce", "users", "locale", "theme"]) params.delete(key);
|
|
1047
|
+
const initial = new Map((hostOptions.databases ?? []).map((database) => [database.info.id, database]));
|
|
1048
|
+
for (const info of testHost.host.listDatabases()) {
|
|
1049
|
+
const requested = initial.get(info.id)?.info.capabilities ?? [];
|
|
1050
|
+
const same = requested.length === info.capabilities.length && requested.every((capability) => info.capabilities.includes(capability));
|
|
1051
|
+
if (!same) params.append("db", `${info.id}:${info.capabilities.join(",")}`);
|
|
1052
|
+
}
|
|
1053
|
+
for (const [id, override] of Object.entries(url.overrides)) {
|
|
1054
|
+
if (override.mapped === false) params.append("db", `${id}:none`);
|
|
1055
|
+
}
|
|
1056
|
+
if (testHost.host.getEnforceCapabilities()) params.set("enforce", "1");
|
|
1057
|
+
const users = testHost.host.getDirectoryUsers().length;
|
|
1058
|
+
if (users) params.set("users", String(users));
|
|
1059
|
+
if (currentLocale) params.set("locale", currentLocale);
|
|
1060
|
+
if (initialDark === void 0 || dark !== initialDark) params.set("theme", dark ? "dark" : "light");
|
|
1061
|
+
const query = params.toString();
|
|
1062
|
+
window.history.replaceState(
|
|
1063
|
+
null,
|
|
1064
|
+
"",
|
|
1065
|
+
`${window.location.pathname}${query ? `?${query}` : ""}${window.location.hash}`
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
function requestEntry(request) {
|
|
1069
|
+
const li = document.createElement("li");
|
|
1070
|
+
const details = document.createElement("details");
|
|
1071
|
+
const response = testHost.log.responses[request.id];
|
|
1072
|
+
const params = request.params ?? {};
|
|
1073
|
+
const summary = document.createElement("summary");
|
|
1074
|
+
const method = document.createElement("span");
|
|
1075
|
+
method.textContent = request.method;
|
|
1076
|
+
if (response && !response.ok) method.className = "htest__error";
|
|
1077
|
+
const meta = document.createElement("span");
|
|
1078
|
+
meta.className = "htest__meta";
|
|
1079
|
+
meta.textContent = [
|
|
1080
|
+
typeof params.databaseId === "string" ? params.databaseId : null,
|
|
1081
|
+
response ? `${response.durationMs.toFixed(1)} ms` : "\u2026"
|
|
1082
|
+
].filter(Boolean).join(" \xB7 ");
|
|
1083
|
+
summary.append(method, meta);
|
|
1084
|
+
details.append(summary);
|
|
1085
|
+
const fill = () => {
|
|
1086
|
+
if (details.childElementCount > 1) return;
|
|
1087
|
+
const body = document.createElement("div");
|
|
1088
|
+
const label = (text) => {
|
|
1089
|
+
const node = document.createElement("div");
|
|
1090
|
+
node.className = "htest__muted";
|
|
1091
|
+
node.textContent = text;
|
|
1092
|
+
return node;
|
|
1093
|
+
};
|
|
1094
|
+
const pre = (text, error = false) => {
|
|
1095
|
+
const node = document.createElement("pre");
|
|
1096
|
+
node.textContent = text;
|
|
1097
|
+
if (error) node.className = "htest__error";
|
|
1098
|
+
return node;
|
|
1099
|
+
};
|
|
1100
|
+
body.append(label("params"), pre(previewJson(request.params ?? null)));
|
|
1101
|
+
if (!response) body.append(label("pending"));
|
|
1102
|
+
else if (response.ok) body.append(label("result"), pre(previewJson(response.result)));
|
|
1103
|
+
else body.append(label("error"), pre(`${response.error.code}: ${response.error.message}`, true));
|
|
1104
|
+
details.append(body);
|
|
1105
|
+
};
|
|
1106
|
+
details.addEventListener("toggle", () => {
|
|
1107
|
+
if (details.open) {
|
|
1108
|
+
openRequests.add(request.id);
|
|
1109
|
+
fill();
|
|
1110
|
+
} else {
|
|
1111
|
+
openRequests.delete(request.id);
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
if (openRequests.has(request.id)) {
|
|
1115
|
+
details.open = true;
|
|
1116
|
+
fill();
|
|
1117
|
+
}
|
|
1118
|
+
li.append(details);
|
|
1119
|
+
return li;
|
|
1120
|
+
}
|
|
1121
|
+
function scanEntry(entry) {
|
|
1122
|
+
const li = document.createElement("li");
|
|
1123
|
+
const details = document.createElement("details");
|
|
1124
|
+
const summary = document.createElement("summary");
|
|
1125
|
+
summary.textContent = `${entry.result} \u2192 ${entry.docId}`;
|
|
1126
|
+
const pre = document.createElement("pre");
|
|
1127
|
+
pre.textContent = previewJson(entry);
|
|
1128
|
+
details.append(summary, pre);
|
|
1129
|
+
li.append(details);
|
|
1130
|
+
return li;
|
|
1131
|
+
}
|
|
638
1132
|
function render() {
|
|
639
1133
|
const { log } = testHost;
|
|
640
1134
|
focusState.textContent = `focused: ${focused ? "yes" : "no"} \xB7 requests: ${log.focusRequests}`;
|
|
@@ -644,10 +1138,19 @@ function mountHavenTestHost(options) {
|
|
|
644
1138
|
lists.previews.replaceChildren(
|
|
645
1139
|
...log.previews.map((entry) => item(`${entry.attachmentName} (${entry.databaseId}/${entry.docId})`))
|
|
646
1140
|
);
|
|
647
|
-
lists.scans.replaceChildren(...log.scans.map(
|
|
648
|
-
lists.requests.replaceChildren(...log.requests.slice(-
|
|
1141
|
+
lists.scans.replaceChildren(...log.scans.map(scanEntry));
|
|
1142
|
+
lists.requests.replaceChildren(...log.requests.slice(-50).reverse().map(requestEntry));
|
|
649
1143
|
}
|
|
650
|
-
const testHost = createBrowserTestHost({
|
|
1144
|
+
const testHost = createBrowserTestHost({
|
|
1145
|
+
...hostOptions,
|
|
1146
|
+
databases,
|
|
1147
|
+
launchContext,
|
|
1148
|
+
enforceCapabilities: url.enforceCapabilities ?? hostOptions.enforceCapabilities,
|
|
1149
|
+
directoryUsers: url.directoryUserCount !== void 0 ? generateDirectoryUsers(url.directoryUserCount) : hostOptions.directoryUsers,
|
|
1150
|
+
frame,
|
|
1151
|
+
onChange: render
|
|
1152
|
+
});
|
|
1153
|
+
let currentLocale = url.locale;
|
|
651
1154
|
const badge = document.createElement("span");
|
|
652
1155
|
badge.className = "htest__badge";
|
|
653
1156
|
badge.textContent = "TEST HOST \xB7 MOCK DATA";
|
|
@@ -660,6 +1163,7 @@ function mountHavenTestHost(options) {
|
|
|
660
1163
|
button("Toggle theme", () => {
|
|
661
1164
|
dark = !dark;
|
|
662
1165
|
testHost.setTheme({ mode: dark ? "dark" : "light", preset: "mindoo" });
|
|
1166
|
+
syncUrl();
|
|
663
1167
|
}),
|
|
664
1168
|
button("Toggle focus", () => {
|
|
665
1169
|
focused = !focused;
|
|
@@ -668,31 +1172,121 @@ function mountHavenTestHost(options) {
|
|
|
668
1172
|
button("Reload app", () => testHost.reloadApp())
|
|
669
1173
|
);
|
|
670
1174
|
section("Host", hostRow, focusState);
|
|
1175
|
+
const language = document.createElement("select");
|
|
1176
|
+
language.dataset.testid = "haven-test-language";
|
|
1177
|
+
language.setAttribute("aria-label", "Language");
|
|
1178
|
+
for (const { tag, label } of locales) language.add(new Option(`${label} (${tag})`, tag));
|
|
1179
|
+
void testHost.host.session.getLaunchContext().then((context) => {
|
|
1180
|
+
if (![...language.options].some((option) => option.value === context.locale)) {
|
|
1181
|
+
language.add(new Option(context.locale, context.locale), 0);
|
|
1182
|
+
}
|
|
1183
|
+
language.value = context.locale;
|
|
1184
|
+
dark = context.theme.mode === "dark";
|
|
1185
|
+
if (!url.theme) initialDark = dark;
|
|
1186
|
+
});
|
|
1187
|
+
language.addEventListener("change", () => {
|
|
1188
|
+
currentLocale = language.value;
|
|
1189
|
+
testHost.setLocale(language.value);
|
|
1190
|
+
syncUrl();
|
|
1191
|
+
});
|
|
1192
|
+
section("Language", language, hint("Haven's locale; the app switches without a reload."));
|
|
1193
|
+
const enforce = document.createElement("label");
|
|
1194
|
+
enforce.className = "htest__check";
|
|
1195
|
+
const enforceBox = document.createElement("input");
|
|
1196
|
+
enforceBox.type = "checkbox";
|
|
1197
|
+
enforceBox.dataset.testid = "haven-test-enforce";
|
|
1198
|
+
enforceBox.checked = testHost.host.getEnforceCapabilities();
|
|
1199
|
+
enforceBox.addEventListener("change", () => {
|
|
1200
|
+
testHost.setEnforceCapabilities(enforceBox.checked);
|
|
1201
|
+
syncUrl();
|
|
1202
|
+
});
|
|
1203
|
+
enforce.append(enforceBox, "Enforce capabilities (reject calls not granted)");
|
|
1204
|
+
const databaseBlocks = testHost.host.listDatabases().map((info) => {
|
|
1205
|
+
const block = document.createElement("div");
|
|
1206
|
+
const name = document.createElement("h3");
|
|
1207
|
+
name.textContent = info.title === info.id ? info.id : `${info.title} (${info.id})`;
|
|
1208
|
+
const grid = document.createElement("div");
|
|
1209
|
+
grid.className = "htest__caps";
|
|
1210
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
1211
|
+
for (const capability of ALL_CAPABILITIES) {
|
|
1212
|
+
const label = document.createElement("label");
|
|
1213
|
+
const box = document.createElement("input");
|
|
1214
|
+
box.type = "checkbox";
|
|
1215
|
+
box.checked = info.capabilities.includes(capability);
|
|
1216
|
+
box.dataset.testid = `haven-test-cap-${info.id}-${capability}`;
|
|
1217
|
+
box.addEventListener("change", () => {
|
|
1218
|
+
const granted = ALL_CAPABILITIES.filter((entry) => boxes.get(entry)?.checked);
|
|
1219
|
+
testHost.setCapabilities(info.id, granted);
|
|
1220
|
+
syncUrl();
|
|
1221
|
+
});
|
|
1222
|
+
boxes.set(capability, box);
|
|
1223
|
+
label.append(box, capability);
|
|
1224
|
+
grid.append(label);
|
|
1225
|
+
}
|
|
1226
|
+
block.append(name, grid);
|
|
1227
|
+
return block;
|
|
1228
|
+
});
|
|
1229
|
+
section(
|
|
1230
|
+
"Databases",
|
|
1231
|
+
enforce,
|
|
1232
|
+
...databaseBlocks,
|
|
1233
|
+
hint("Changing a capability relaunches the app. Unmap a database with ?db=<id>:none in the URL.")
|
|
1234
|
+
);
|
|
1235
|
+
const directory = document.createElement("select");
|
|
1236
|
+
directory.dataset.testid = "haven-test-directory";
|
|
1237
|
+
directory.setAttribute("aria-label", "Directory users");
|
|
1238
|
+
const userCount = testHost.host.getDirectoryUsers().length;
|
|
1239
|
+
const sizes = DIRECTORY_SIZES.includes(userCount) ? DIRECTORY_SIZES : [...DIRECTORY_SIZES, userCount];
|
|
1240
|
+
for (const size of sizes.sort((a, b) => a - b)) {
|
|
1241
|
+
directory.add(new Option(size === userCount && !DIRECTORY_SIZES.includes(size) ? `${size} (custom)` : `${size} users`, String(size)));
|
|
1242
|
+
}
|
|
1243
|
+
directory.value = String(userCount);
|
|
1244
|
+
directory.addEventListener("change", () => {
|
|
1245
|
+
testHost.setDirectoryUsers(generateDirectoryUsers(Number(directory.value)));
|
|
1246
|
+
syncUrl();
|
|
1247
|
+
});
|
|
1248
|
+
section("Directory", directory, hint("Usernames directory.listUsers() returns, e.g. to test paging."));
|
|
1249
|
+
const scan = document.createElement("select");
|
|
1250
|
+
scan.dataset.testid = "haven-test-scan-mode";
|
|
1251
|
+
scan.setAttribute("aria-label", "Scanner result");
|
|
1252
|
+
scan.add(new Option("Cancel the scan", "cancel"));
|
|
1253
|
+
scan.add(new Option("Sample image", "sample"));
|
|
1254
|
+
scan.add(new Option("Chosen file", "file"));
|
|
671
1255
|
const scanInput = document.createElement("input");
|
|
672
1256
|
scanInput.type = "file";
|
|
673
1257
|
scanInput.accept = "image/jpeg,image/png,application/pdf";
|
|
674
|
-
|
|
1258
|
+
const initialMode = testHost.getScanMode();
|
|
1259
|
+
scan.value = typeof initialMode === "string" ? initialMode : "file";
|
|
1260
|
+
scanInput.hidden = scan.value !== "file";
|
|
1261
|
+
const applyScanMode = async () => {
|
|
1262
|
+
scanInput.hidden = scan.value !== "file";
|
|
1263
|
+
if (scan.value !== "file") {
|
|
1264
|
+
testHost.setScanMode(scan.value);
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
675
1267
|
const file = scanInput.files?.[0];
|
|
676
|
-
testHost.
|
|
1268
|
+
testHost.setScanMode(
|
|
677
1269
|
file ? {
|
|
678
1270
|
fileName: file.name,
|
|
679
1271
|
mimeType: file.type === "image/png" || file.type === "application/pdf" ? file.type : "image/jpeg",
|
|
680
|
-
|
|
681
|
-
} :
|
|
1272
|
+
bytes: new Uint8Array(await file.arrayBuffer())
|
|
1273
|
+
} : "cancel"
|
|
682
1274
|
);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
1275
|
+
};
|
|
1276
|
+
scan.addEventListener("change", () => void applyScanMode());
|
|
1277
|
+
scanInput.addEventListener("change", () => void applyScanMode());
|
|
1278
|
+
section(
|
|
1279
|
+
"Scanner",
|
|
1280
|
+
scan,
|
|
1281
|
+
scanInput,
|
|
1282
|
+
hint("What attachments.scan() returns; the file is attached to the document like in Haven.")
|
|
1283
|
+
);
|
|
688
1284
|
section("Notifications", lists.notifications);
|
|
689
1285
|
section("Previews", lists.previews);
|
|
690
1286
|
section("Scans", lists.scans);
|
|
691
|
-
section("Requests (latest first)", lists.requests);
|
|
692
|
-
void testHost.host.session.getLaunchContext().then((context) => {
|
|
693
|
-
dark = context.theme.mode === "dark";
|
|
694
|
-
});
|
|
1287
|
+
section("Requests (latest first, click for details)", lists.requests);
|
|
695
1288
|
render();
|
|
1289
|
+
syncUrl();
|
|
696
1290
|
window.__havenTestHost = testHost;
|
|
697
1291
|
return testHost;
|
|
698
1292
|
}
|
|
@@ -1864,7 +2458,7 @@ async function mockSha256Hex(value) {
|
|
|
1864
2458
|
const digest = await subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
1865
2459
|
return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1866
2460
|
}
|
|
1867
|
-
function createDatabaseHandle(definition, peers) {
|
|
2461
|
+
function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
1868
2462
|
let createCounter = 0;
|
|
1869
2463
|
let changeCounter = 0;
|
|
1870
2464
|
const defaultViewFactory = async () => createDefaultViewNavigator();
|
|
@@ -2430,9 +3024,7 @@ function createDatabaseHandle(definition, peers) {
|
|
|
2430
3024
|
status: "unknown"
|
|
2431
3025
|
}));
|
|
2432
3026
|
},
|
|
2433
|
-
async
|
|
2434
|
-
return [];
|
|
2435
|
-
}
|
|
3027
|
+
listUsers: (async (options) => options ? pageUsers(directoryUsers(), options) : [...directoryUsers()])
|
|
2436
3028
|
};
|
|
2437
3029
|
const defaultViews = {
|
|
2438
3030
|
async create(_input) {
|
|
@@ -2650,15 +3242,23 @@ function createMockSessionState(options = {}) {
|
|
|
2650
3242
|
capabilities: [...definition.info.capabilities]
|
|
2651
3243
|
}));
|
|
2652
3244
|
for (const definition of definitions) {
|
|
2653
|
-
const created = createDatabaseHandle(definition, databasePeers);
|
|
2654
|
-
|
|
3245
|
+
const created = createDatabaseHandle(definition, databasePeers, () => directoryUsers);
|
|
3246
|
+
const databaseId = definition.info.id;
|
|
3247
|
+
databaseHandles.set(
|
|
3248
|
+
databaseId,
|
|
3249
|
+
guardDatabase(
|
|
3250
|
+
created.handle,
|
|
3251
|
+
() => databaseInfos.find((entry) => entry.id === databaseId) ?? definition.info,
|
|
3252
|
+
() => enforceCapabilities
|
|
3253
|
+
)
|
|
3254
|
+
);
|
|
2655
3255
|
databaseViewDocumentLists.set(definition.info.id, created.listViewDocuments);
|
|
2656
3256
|
databaseDocumentStores.set(definition.info.id, created.documents);
|
|
2657
3257
|
databaseLiveQueryRefreshers.set(definition.info.id, created.notifyLiveQueries);
|
|
2658
3258
|
databaseViewApis.set(definition.info.id, {
|
|
2659
3259
|
async create(input) {
|
|
2660
|
-
const documents = input.databaseIds.flatMap((
|
|
2661
|
-
const list = databaseViewDocumentLists.get(
|
|
3260
|
+
const documents = input.databaseIds.flatMap((databaseId2) => {
|
|
3261
|
+
const list = databaseViewDocumentLists.get(databaseId2);
|
|
2662
3262
|
return list ? list() : [];
|
|
2663
3263
|
});
|
|
2664
3264
|
const evaluating = await createEvaluatingViewNavigator(input, documents);
|
|
@@ -2678,6 +3278,16 @@ function createMockSessionState(options = {}) {
|
|
|
2678
3278
|
databases: databaseInfos
|
|
2679
3279
|
});
|
|
2680
3280
|
};
|
|
3281
|
+
let enforceCapabilities = options.enforceCapabilities ?? false;
|
|
3282
|
+
let directoryUsers = [...options.directoryUsers ?? []];
|
|
3283
|
+
const setCapabilities = (databaseId, capabilities) => {
|
|
3284
|
+
const entry = databaseInfos.find((info) => info.id === databaseId);
|
|
3285
|
+
if (!entry) {
|
|
3286
|
+
throw new Error(`Unknown test database: ${databaseId}`);
|
|
3287
|
+
}
|
|
3288
|
+
entry.capabilities = [...new Set(capabilities)];
|
|
3289
|
+
launchContext = mergeLaunchContext(launchContext, { databases: databaseInfos });
|
|
3290
|
+
};
|
|
2681
3291
|
setDatabases(options.databases ?? []);
|
|
2682
3292
|
const session = {
|
|
2683
3293
|
async getLaunchContext() {
|
|
@@ -2885,6 +3495,15 @@ function createMockSessionState(options = {}) {
|
|
|
2885
3495
|
}));
|
|
2886
3496
|
},
|
|
2887
3497
|
setDatabases,
|
|
3498
|
+
setCapabilities,
|
|
3499
|
+
setEnforceCapabilities(enforce) {
|
|
3500
|
+
enforceCapabilities = enforce;
|
|
3501
|
+
},
|
|
3502
|
+
getEnforceCapabilities: () => enforceCapabilities,
|
|
3503
|
+
setDirectoryUsers(users) {
|
|
3504
|
+
directoryUsers = [...users];
|
|
3505
|
+
},
|
|
3506
|
+
getDirectoryUsers: () => [...directoryUsers],
|
|
2888
3507
|
getDatabase(databaseId) {
|
|
2889
3508
|
const database = databaseHandles.get(databaseId);
|
|
2890
3509
|
if (!database) {
|
|
@@ -2936,6 +3555,15 @@ function createMockSessionState(options = {}) {
|
|
|
2936
3555
|
}
|
|
2937
3556
|
};
|
|
2938
3557
|
}
|
|
3558
|
+
function generateDirectoryUsers(count, options = {}) {
|
|
3559
|
+
const prefix = options.prefix ?? "Test User";
|
|
3560
|
+
const organization = options.organization ?? "Test";
|
|
3561
|
+
const width = String(Math.max(count, 1)).length;
|
|
3562
|
+
return Array.from(
|
|
3563
|
+
{ length: count },
|
|
3564
|
+
(_, index) => `CN=${prefix} ${String(index + 1).padStart(width, "0")}/O=${organization}`
|
|
3565
|
+
);
|
|
3566
|
+
}
|
|
2939
3567
|
function createMockMindooDBAppSession(options = {}) {
|
|
2940
3568
|
const state = createMockSessionState(options);
|
|
2941
3569
|
return {
|
|
@@ -2947,6 +3575,9 @@ function createMockMindooDBAppSession(options = {}) {
|
|
|
2947
3575
|
proposedApps: state.proposedApps,
|
|
2948
3576
|
listDatabases: state.listDatabaseInfos,
|
|
2949
3577
|
setDatabases: state.setDatabases,
|
|
3578
|
+
setCapabilities: state.setCapabilities,
|
|
3579
|
+
setEnforceCapabilities: state.setEnforceCapabilities,
|
|
3580
|
+
setDirectoryUsers: state.setDirectoryUsers,
|
|
2950
3581
|
emitThemeChange: state.emitThemeChange,
|
|
2951
3582
|
emitViewportChange: state.emitViewportChange,
|
|
2952
3583
|
emitUiPreferencesChange: state.emitUiPreferencesChange,
|
|
@@ -3392,8 +4023,16 @@ function createFakeBridgeHost(options = {}) {
|
|
|
3392
4023
|
return await state.getDatabase(String(params.databaseId)).directory.excerpt(
|
|
3393
4024
|
Array.isArray(params.publicKeys) ? params.publicKeys.map(String) : []
|
|
3394
4025
|
);
|
|
3395
|
-
case "directory.listUsers":
|
|
3396
|
-
|
|
4026
|
+
case "directory.listUsers": {
|
|
4027
|
+
const directory = state.getDatabase(String(params.databaseId)).directory;
|
|
4028
|
+
const paged = params.query !== void 0 || params.cursor !== void 0 || params.limit !== void 0;
|
|
4029
|
+
if (!paged) return await directory.listUsers();
|
|
4030
|
+
return await directory.listUsers({
|
|
4031
|
+
query: typeof params.query === "string" ? params.query : void 0,
|
|
4032
|
+
cursor: typeof params.cursor === "string" ? params.cursor : void 0,
|
|
4033
|
+
limit: typeof params.limit === "number" ? params.limit : void 0
|
|
4034
|
+
});
|
|
4035
|
+
}
|
|
3397
4036
|
case "attachments.list":
|
|
3398
4037
|
return await state.getDatabase(String(params.databaseId)).attachments.list(
|
|
3399
4038
|
String(params.docId),
|
|
@@ -3674,6 +4313,23 @@ function createFakeBridgeHost(options = {}) {
|
|
|
3674
4313
|
if (message.kind === "request") {
|
|
3675
4314
|
requests.push(message);
|
|
3676
4315
|
options.onRequest?.(message);
|
|
4316
|
+
const startedAt = performance.now();
|
|
4317
|
+
const succeed = (result) => {
|
|
4318
|
+
postRpcSuccess(port, message.id, result);
|
|
4319
|
+
options.onResponse?.(message, {
|
|
4320
|
+
ok: true,
|
|
4321
|
+
result,
|
|
4322
|
+
durationMs: performance.now() - startedAt
|
|
4323
|
+
});
|
|
4324
|
+
};
|
|
4325
|
+
const fail = (error) => {
|
|
4326
|
+
postRpcError(port, message.id, error);
|
|
4327
|
+
options.onResponse?.(message, {
|
|
4328
|
+
ok: false,
|
|
4329
|
+
error,
|
|
4330
|
+
durationMs: performance.now() - startedAt
|
|
4331
|
+
});
|
|
4332
|
+
};
|
|
3677
4333
|
try {
|
|
3678
4334
|
const customHandler = customRequestHandlers.get(message.method);
|
|
3679
4335
|
if (customHandler) {
|
|
@@ -3682,20 +4338,20 @@ function createFakeBridgeHost(options = {}) {
|
|
|
3682
4338
|
request: message,
|
|
3683
4339
|
port
|
|
3684
4340
|
});
|
|
3685
|
-
|
|
4341
|
+
succeed(result);
|
|
3686
4342
|
return;
|
|
3687
4343
|
}
|
|
3688
4344
|
const builtinResult = await handleBuiltinRequest(message);
|
|
3689
4345
|
if (builtinResult === void 0 && message.method !== "viewNavigators.dispose") {
|
|
3690
|
-
|
|
4346
|
+
fail({
|
|
3691
4347
|
code: "method-not-found",
|
|
3692
4348
|
message: `No fake bridge handler is configured for ${message.method}.`
|
|
3693
4349
|
});
|
|
3694
4350
|
return;
|
|
3695
4351
|
}
|
|
3696
|
-
|
|
4352
|
+
succeed(builtinResult);
|
|
3697
4353
|
} catch (error) {
|
|
3698
|
-
|
|
4354
|
+
fail(createBridgeErrorPayload(error));
|
|
3699
4355
|
}
|
|
3700
4356
|
return;
|
|
3701
4357
|
}
|
|
@@ -3737,6 +4393,12 @@ function createFakeBridgeHost(options = {}) {
|
|
|
3737
4393
|
},
|
|
3738
4394
|
install,
|
|
3739
4395
|
acceptConnection,
|
|
4396
|
+
listDatabases: state.listDatabaseInfos,
|
|
4397
|
+
setCapabilities: state.setCapabilities,
|
|
4398
|
+
setEnforceCapabilities: state.setEnforceCapabilities,
|
|
4399
|
+
getEnforceCapabilities: state.getEnforceCapabilities,
|
|
4400
|
+
setDirectoryUsers: state.setDirectoryUsers,
|
|
4401
|
+
getDirectoryUsers: state.getDirectoryUsers,
|
|
3740
4402
|
dispose() {
|
|
3741
4403
|
restoreWindow();
|
|
3742
4404
|
connectedPorts.forEach((port) => {
|
|
@@ -3853,13 +4515,20 @@ function createFakeBridgeHost(options = {}) {
|
|
|
3853
4515
|
return controller;
|
|
3854
4516
|
}
|
|
3855
4517
|
export {
|
|
4518
|
+
ALL_CAPABILITIES,
|
|
4519
|
+
MockForbiddenError,
|
|
4520
|
+
TEST_HOST_LOCALES,
|
|
3856
4521
|
capabilitiesForDefinitionPermissions,
|
|
3857
4522
|
createBrowserTestHost,
|
|
3858
4523
|
createEvaluatingViewNavigator,
|
|
3859
4524
|
createFakeBridgeHost,
|
|
4525
|
+
createMemoryAttachments,
|
|
3860
4526
|
createMockMindooDBAppBridge,
|
|
3861
4527
|
createMockMindooDBAppSession,
|
|
4528
|
+
generateDirectoryUsers,
|
|
3862
4529
|
loadMindoodbForTesting,
|
|
3863
4530
|
mockDatabasesFromDefinition,
|
|
3864
|
-
mountHavenTestHost
|
|
4531
|
+
mountHavenTestHost,
|
|
4532
|
+
previewJson,
|
|
4533
|
+
readTestHostUrlSettings
|
|
3865
4534
|
};
|