@synopackageland/templates 0.9.9 → 0.10.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/README.md +29 -29
- package/package.json +2 -2
- package/templates/_shared/AGENTS.md +53 -15
- package/templates/native-go/backend/THIRD_PARTY_NOTICES.txt +1 -1
- package/templates/native-go/backend/cmd/app/main.go +15 -1
- package/templates/native-go/backend/cmd/app/main_test.go +33 -0
- package/templates/native-go/backend/go.mod +1 -1
- package/templates/native-go/backend/go.sum +2 -2
- package/templates/native-go/i18n/messages/de.json +4 -0
- package/templates/native-go/i18n/messages/en.json +4 -0
- package/templates/native-go/i18n/messages/es.json +4 -0
- package/templates/native-go/i18n/messages/fr.json +4 -0
- package/templates/native-go/i18n/messages/it.json +4 -0
- package/templates/native-go/i18n/messages/ja.json +4 -0
- package/templates/native-go/i18n/messages/ko.json +4 -0
- package/templates/native-go/i18n/messages/zh-CN.json +4 -0
- package/templates/native-go/i18n/messages/zh-TW.json +4 -0
- package/templates/native-go/ui/app.js +2 -1
- package/templates/native-go-delegate/frontend/test/capability-panels.test.ts +24 -3
- package/templates/native-go-jobs/frontend/test/background-page.test.ts +4 -4
- package/templates/native-go-root-ops/backend/internal/app/executor.go +2 -65
- package/templates/native-go-root-ops/backend/internal/app/executor_test.go +10 -82
- package/templates/native-go-root-ops/frontend/src/ExecutorGate.vue +16 -0
- package/templates/native-go-root-ops/frontend/test/executor-gate.test.ts +60 -0
- package/templates/native-go-vue/README.md +24 -8
- package/templates/native-go-vue/frontend/package-lock.json +9 -9
- package/templates/native-go-vue/frontend/package.json +2 -2
- package/templates/native-go-vue/frontend/src/App.vue +36 -36
- package/templates/native-go-vue/frontend/src/ExecutorGate.vue +10 -0
- package/templates/native-go-vue/frontend/test/apps.test.ts +65 -12
- package/templates/native-go-vue/frontend/test/context.ts +8 -0
- package/templates/native-go-vue/frontend/test/native-controls.test.ts +3 -4
- package/templates/native-go-vue/frontend/test/shell-i18n.test.ts +2 -2
- package/templates/native-node/README.md +153 -0
- package/templates/native-node/backend/app/package.json +4 -0
- package/templates/native-node/backend/app/permissions.json +5 -0
- package/templates/native-node/backend/package-lock.json +59 -0
- package/templates/native-node/backend/package.json +17 -0
- package/templates/native-node/backend/src/delegate.ts +5 -0
- package/templates/native-node/backend/src/jobs.ts +5 -0
- package/templates/native-node/backend/src/main.ts +4 -0
- package/templates/native-node/backend/src/params.ts +11 -0
- package/templates/native-node/backend/src/root-ops.ts +5 -0
- package/templates/native-node/backend/src/routes.ts +97 -0
- package/templates/native-node/backend/src/service.ts +22 -0
- package/templates/native-node/backend/test/routes.test.ts +75 -0
- package/templates/native-node/backend/test/support.ts +67 -0
- package/templates/native-node/backend/tsconfig.build.json +8 -0
- package/templates/native-node/backend/tsconfig.json +18 -0
- package/templates/native-node/dsm-package.yaml +80 -0
- package/templates/native-node/gitignore +4 -0
- package/templates/native-node-delegate/backend/src/delegate.ts +35 -0
- package/templates/native-node-delegate/backend/test/delegate.test.ts +51 -0
- package/templates/native-node-jobs/backend/src/jobs.ts +38 -0
- package/templates/native-node-jobs/backend/test/jobs.test.ts +73 -0
- package/templates/native-node-root-ops/backend/src/root-ops.ts +16 -0
- package/templates/native-node-root-ops/backend/test/root-ops.test.ts +23 -0
- package/templates/native-node-vue/README.md +56 -0
- package/templates/native-node-vue/gitignore +4 -0
- package/templates/native-python/README.md +150 -0
- package/templates/native-python/backend/app/delegate.py +5 -0
- package/templates/native-python/backend/app/jobs.py +5 -0
- package/templates/native-python/backend/app/main.py +5 -0
- package/templates/native-python/backend/app/params.py +10 -0
- package/templates/native-python/backend/app/permissions.json +5 -0
- package/templates/native-python/backend/app/root_ops.py +5 -0
- package/templates/native-python/backend/app/routes.py +107 -0
- package/templates/native-python/backend/tests/support.py +72 -0
- package/templates/native-python/backend/tests/test_routes.py +73 -0
- package/templates/native-python/dsm-package.yaml +77 -0
- package/templates/native-python/gitignore +2 -0
- package/templates/native-python-delegate/backend/app/delegate.py +42 -0
- package/templates/native-python-delegate/backend/tests/test_delegate.py +58 -0
- package/templates/native-python-jobs/backend/app/jobs.py +40 -0
- package/templates/native-python-jobs/backend/tests/test_jobs.py +72 -0
- package/templates/native-python-root-ops/backend/app/root_ops.py +18 -0
- package/templates/native-python-root-ops/backend/tests/test_root_ops.py +34 -0
- package/templates/native-python-vue/README.md +56 -0
- package/templates/native-python-vue/gitignore +3 -0
- package/templates/native-rust/README.md +74 -296
- package/templates/native-rust/backend/Cargo.lock +6 -6
- package/templates/native-rust/backend/Cargo.toml +2 -10
- package/templates/native-rust/backend/src/delegate.rs +6 -0
- package/templates/native-rust/backend/src/jobs.rs +6 -0
- package/templates/native-rust/backend/src/main.rs +12 -0
- package/templates/native-rust/backend/src/root_ops.rs +6 -0
- package/templates/native-rust/backend/src/routes.rs +352 -0
- package/templates/native-rust/backend/src/test_support.rs +91 -0
- package/templates/native-rust/dsm-package.yaml +10 -21
- package/templates/native-rust-delegate/backend/src/delegate.rs +171 -0
- package/templates/native-rust-jobs/backend/src/jobs.rs +240 -0
- package/templates/native-rust-root-ops/backend/src/root_ops.rs +83 -0
- package/templates/native-rust-vue/README.md +35 -34
- package/templates/native-rust/backend/THIRD_PARTY_NOTICES.txt +0 -207
- package/templates/native-rust/backend/src/app/background/mod.rs +0 -424
- package/templates/native-rust/backend/src/app/background/owner.rs +0 -77
- package/templates/native-rust/backend/src/app/background/owner_tests.rs +0 -174
- package/templates/native-rust/backend/src/app/background/schedule.rs +0 -126
- package/templates/native-rust/backend/src/app/background/schedule_tests.rs +0 -108
- package/templates/native-rust/backend/src/app/background/tests.rs +0 -505
- package/templates/native-rust/backend/src/app/background/work.rs +0 -78
- package/templates/native-rust/backend/src/app/background/work_tests.rs +0 -123
- package/templates/native-rust/backend/src/app/background_routes.rs +0 -22
- package/templates/native-rust/backend/src/app/delegate_config.rs +0 -188
- package/templates/native-rust/backend/src/app/delegate_routes.rs +0 -22
- package/templates/native-rust/backend/src/app/identity.rs +0 -56
- package/templates/native-rust/backend/src/app/migrations.rs +0 -79
- package/templates/native-rust/backend/src/app/mod.rs +0 -30
- package/templates/native-rust/backend/src/app/portal.rs +0 -317
- package/templates/native-rust/backend/src/app/root_routes.rs +0 -23
- package/templates/native-rust/backend/src/app/routes.rs +0 -315
- package/templates/native-rust/backend/src/app/session_files.rs +0 -19
- package/templates/native-rust/backend/src/app/shared_folder.rs +0 -383
- package/templates/native-rust/backend/src/app/test_support.rs +0 -202
- package/templates/native-rust/backend/src/app/uninstall.rs +0 -38
- package/templates/native-rust/backend/src/bin/api.rs +0 -72
- package/templates/native-rust/backend/src/bin/background-worker.rs +0 -37
- package/templates/native-rust/backend/src/build_env.rs +0 -155
- package/templates/native-rust/backend/src/lib.rs +0 -6
- package/templates/native-rust/backend/src/permissions.rs +0 -54
- package/templates/native-rust/backend/src/process.rs +0 -91
- package/templates/native-rust/i18n/glossary.json +0 -665
- package/templates/native-rust/i18n/messages/de.json +0 -32
- package/templates/native-rust/i18n/messages/en.json +0 -32
- package/templates/native-rust/i18n/messages/es.json +0 -32
- package/templates/native-rust/i18n/messages/fr.json +0 -32
- package/templates/native-rust/i18n/messages/it.json +0 -32
- package/templates/native-rust/i18n/messages/ja.json +0 -32
- package/templates/native-rust/i18n/messages/ko.json +0 -32
- package/templates/native-rust/i18n/messages/zh-CN.json +0 -32
- package/templates/native-rust/i18n/messages/zh-TW.json +0 -32
- package/templates/native-rust/ui/app.css +0 -45
- package/templates/native-rust/ui/app.js +0 -111
- package/templates/native-rust-delegate/backend/src/app/delegate_routes.rs +0 -92
- package/templates/native-rust-delegate/backend/src/bin/background-worker.rs +0 -52
- package/templates/native-rust-jobs/backend/src/app/background_routes.rs +0 -109
- package/templates/native-rust-root-ops/backend/src/app/root_routes.rs +0 -475
|
@@ -20,39 +20,20 @@ func init() {
|
|
|
20
20
|
sampleOutcome["root.run"] = "session_required"
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if
|
|
32
|
-
t.
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
for _, raw := range []string{`{"action":"start;id"}`, `{"action":"start\nstop"}`, `{"action":""}`, `{"action":"stop","path":"/etc/passwd"}`} {
|
|
36
|
-
if code, _ := call(t, cfg, alice, "executor.control", raw); code != "bad_request" {
|
|
37
|
-
t.Fatalf("accepted %s: %s", raw, code)
|
|
23
|
+
// The SDK serves executor.status and executor.control and tests them; the
|
|
24
|
+
// App only registers them.
|
|
25
|
+
func TestTheSDKsExecutorRoutesAreRegistered(t *testing.T) {
|
|
26
|
+
registered := map[string]bool{}
|
|
27
|
+
for _, r := range Routes(testConfig(t)) {
|
|
28
|
+
registered[r.Method()] = true
|
|
29
|
+
}
|
|
30
|
+
for _, method := range []string{"executor.status", "executor.control", "root.run"} {
|
|
31
|
+
if !registered[method] {
|
|
32
|
+
t.Errorf("%s is not registered", method)
|
|
38
33
|
}
|
|
39
34
|
}
|
|
40
35
|
}
|
|
41
36
|
|
|
42
|
-
func TestExecutorControlRejectsSymlinkDirectory(t *testing.T) {
|
|
43
|
-
cfg := testConfig(t)
|
|
44
|
-
outside := t.TempDir()
|
|
45
|
-
if err := os.Symlink(outside, filepath.Join(cfg.DataDir, "executor")); err != nil {
|
|
46
|
-
t.Fatal(err)
|
|
47
|
-
}
|
|
48
|
-
if code, _ := call(t, cfg, alice, "executor.control", `{"action":"stop"}`); code == "" {
|
|
49
|
-
t.Fatal("accepted symlink")
|
|
50
|
-
}
|
|
51
|
-
if _, err := os.Stat(filepath.Join(outside, "control")); !os.IsNotExist(err) {
|
|
52
|
-
t.Fatal("wrote outside data directory")
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
37
|
func TestExecutorRootRunRequiresCurrentDSMCookie(t *testing.T) {
|
|
57
38
|
t.Setenv("HTTP_COOKIE", "")
|
|
58
39
|
if code, _ := call(t, testConfig(t), alice, "root.run", `{"name":"whoami"}`); code == "" {
|
|
@@ -60,59 +41,6 @@ func TestExecutorRootRunRequiresCurrentDSMCookie(t *testing.T) {
|
|
|
60
41
|
}
|
|
61
42
|
}
|
|
62
43
|
|
|
63
|
-
// The executor closes its listener while paused (package stopped, or pins
|
|
64
|
-
// awaiting administrator approval). Status then answers, not "internal".
|
|
65
|
-
func TestExecutorStatusReportsAPausedExecutor(t *testing.T) {
|
|
66
|
-
cfg := testConfig(t)
|
|
67
|
-
useExecutorSocketBase(t)
|
|
68
|
-
check := func(name string) {
|
|
69
|
-
t.Helper()
|
|
70
|
-
code, envelope := call(t, cfg, alice, "executor.status", `{}`)
|
|
71
|
-
out, _ := envelope["data"].(map[string]any)
|
|
72
|
-
if code != "" || out["ready"] != false || out["state"] != "paused" || out["reason"] != "executor_not_listening" || out["next"] == "" || out["next"] == nil {
|
|
73
|
-
t.Fatalf("%s: %s %v", name, code, out)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
check("no socket")
|
|
77
|
-
if code, _ := call(t, cfg, bob, "executor.status", `{}`); code != "" {
|
|
78
|
-
t.Fatal("authenticated user cannot read a paused status", code)
|
|
79
|
-
}
|
|
80
|
-
dir := filepath.Join(executorSocketBase, cfg.PackageID)
|
|
81
|
-
os.MkdirAll(dir, 0700)
|
|
82
|
-
ln, err := net.ListenUnix("unix", &net.UnixAddr{Name: filepath.Join(dir, "executor.sock"), Net: "unix"})
|
|
83
|
-
if err != nil {
|
|
84
|
-
t.Fatal(err)
|
|
85
|
-
}
|
|
86
|
-
ln.SetUnlinkOnClose(false)
|
|
87
|
-
ln.Close()
|
|
88
|
-
check("closed listener")
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// next follows the last lifecycle request: a running package needs approval
|
|
92
|
-
// again, not a start.
|
|
93
|
-
func TestExecutorStatusNextFollowsTheLifecycleRequest(t *testing.T) {
|
|
94
|
-
cfg := testConfig(t)
|
|
95
|
-
useExecutorSocketBase(t)
|
|
96
|
-
next := func() string {
|
|
97
|
-
t.Helper()
|
|
98
|
-
_, envelope := call(t, cfg, alice, "executor.status", `{}`)
|
|
99
|
-
out, _ := envelope["data"].(map[string]any)
|
|
100
|
-
s, _ := out["next"].(string)
|
|
101
|
-
return s
|
|
102
|
-
}
|
|
103
|
-
if !strings.HasPrefix(next(), "Start the package.") {
|
|
104
|
-
t.Fatal("no request:", next())
|
|
105
|
-
}
|
|
106
|
-
mailbox := filepath.Join(cfg.DataDir, "executor")
|
|
107
|
-
os.MkdirAll(mailbox, 0700)
|
|
108
|
-
for request, start := range map[string]bool{"stop\n": true, "start\n": false, "start": false} {
|
|
109
|
-
os.WriteFile(filepath.Join(mailbox, "control"), []byte(request), 0600)
|
|
110
|
-
if got := next(); strings.HasPrefix(got, "Start the package") != start || got == "" {
|
|
111
|
-
t.Fatalf("%q: %s", request, got)
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
44
|
// fakeExecutor answers each v1 request with replies[op] and records the ops.
|
|
117
45
|
type fakeExecutor struct {
|
|
118
46
|
mu sync.Mutex
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// A root-ops App's pages wait for its executor: the SDK's ExecutorApproval
|
|
3
|
+
// takes their place until an administrator approves it with the DSM
|
|
4
|
+
// password, and tells anyone else to ask one. Without ctx.executor (a dev
|
|
5
|
+
// server) there is nothing to approve.
|
|
6
|
+
export const holdsPages = true
|
|
7
|
+
</script>
|
|
8
|
+
<script setup lang="ts">
|
|
9
|
+
import type {Context} from '@synopackageland/sdk'
|
|
10
|
+
import {ExecutorApproval} from '@synopackageland/sdk/vue'
|
|
11
|
+
defineProps<{ctx:Context;admin:boolean}>()
|
|
12
|
+
</script>
|
|
13
|
+
<template>
|
|
14
|
+
<ExecutorApproval v-if="ctx.executor" :admin="admin" :locale="ctx.locale" :ctx="ctx"><slot /></ExecutorApproval>
|
|
15
|
+
<slot v-else />
|
|
16
|
+
</template>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import {afterEach,expect,test,vi} from 'vitest'
|
|
3
|
+
import {nextTick} from 'vue'
|
|
4
|
+
import type {Context} from '@synopackageland/sdk'
|
|
5
|
+
import {mount} from '../src/main'
|
|
6
|
+
import {dsmWindow} from './context'
|
|
7
|
+
const cleanup:Array<()=>void>=[]
|
|
8
|
+
afterEach(()=>cleanup.splice(0).forEach(fn=>fn()))
|
|
9
|
+
async function settle(){for(let i=0;i<30;i++){await Promise.resolve();await nextTick()}}
|
|
10
|
+
function open(admin:boolean,executor?:Context['executor']){
|
|
11
|
+
const call=vi.fn(async(method:string)=>{
|
|
12
|
+
if(method==='app.status')return {package:'Root',version:'1',user:admin?'admin':'alice',admin}
|
|
13
|
+
if(method==='greeting.get')return {text:'Hello Root'}
|
|
14
|
+
if(method==='delegate.status')return {state:'ready',disabled:false,lifetime:'until-revoked',destinations:['save']}
|
|
15
|
+
if(method==='delegate.destination')return {key:'save',selection:{scope:'home',root:'/home',generation:0},destination:null,status:'missing'}
|
|
16
|
+
if(method==='delegate.list')return {delegations:[]}
|
|
17
|
+
if(method==='delegate.operations.list')return {operations:[]}
|
|
18
|
+
throw {code:'unknown_method'}
|
|
19
|
+
})
|
|
20
|
+
const root=document.createElement('div');document.body.append(root)
|
|
21
|
+
const app=mount(root,dsmWindow({call,user:{name:admin?'admin':'alice',isAdmin:admin},executor}))
|
|
22
|
+
cleanup.push(()=>{app.unmount();root.remove()})
|
|
23
|
+
return root
|
|
24
|
+
}
|
|
25
|
+
function awaitingApproval(){
|
|
26
|
+
let state:'needs-admin-approval'|'ready'='needs-admin-approval'
|
|
27
|
+
return {status:vi.fn(async()=>({state})),authorize:vi.fn(async()=>{state='ready';return {state}}),control:vi.fn(async()=>({state}))}
|
|
28
|
+
}
|
|
29
|
+
test('the App pages wait until an administrator approves the executor with the DSM password, in the window',async()=>{
|
|
30
|
+
const executor=awaitingApproval()
|
|
31
|
+
const root=open(true,executor as unknown as Context['executor']);await settle()
|
|
32
|
+
expect(root.textContent).not.toContain('Package status')
|
|
33
|
+
expect(root.querySelector('[role=dialog]')).toBeNull()
|
|
34
|
+
const password=root.querySelector<HTMLInputElement>('input[type=password]')!
|
|
35
|
+
expect(password.closest('[data-ui-app]')).not.toBeNull()
|
|
36
|
+
password.value='one-time';password.dispatchEvent(new Event('input',{bubbles:true}));await settle()
|
|
37
|
+
password.closest('form')!.dispatchEvent(new Event('submit',{bubbles:true,cancelable:true}));await settle()
|
|
38
|
+
expect(executor.authorize).toHaveBeenCalledWith('one-time')
|
|
39
|
+
expect(root.textContent).toContain('Package status')
|
|
40
|
+
expect((root.querySelector('textarea') as HTMLTextAreaElement).value).toBe('Hello Root')
|
|
41
|
+
})
|
|
42
|
+
test('anyone else is told to ask an administrator for system access',async()=>{
|
|
43
|
+
const root=open(false,awaitingApproval() as unknown as Context['executor']);await settle()
|
|
44
|
+
expect(root.textContent).toContain('An administrator must approve system access')
|
|
45
|
+
expect(root.querySelector('input[type=password]')).toBeNull()
|
|
46
|
+
expect(root.textContent).not.toContain('Package status')
|
|
47
|
+
})
|
|
48
|
+
test('without an executor (a dev server) the App pages show at once',async()=>{
|
|
49
|
+
const root=open(false);await settle()
|
|
50
|
+
expect(root.textContent).toContain('Package status')
|
|
51
|
+
expect(root.textContent).not.toContain('An administrator must approve system access')
|
|
52
|
+
})
|
|
53
|
+
// ExecutorApproval holds the pages: AppShell's ExecutorNotice would only repeat it.
|
|
54
|
+
test('while the executor awaits approval no notice repeats the approval page',async()=>{
|
|
55
|
+
for(const admin of [true,false]){
|
|
56
|
+
const root=open(admin,awaitingApproval() as unknown as Context['executor']);await settle()
|
|
57
|
+
expect(root.textContent,String(admin)).toContain(admin?'Approve system access':'An administrator must approve system access')
|
|
58
|
+
expect(root.textContent,String(admin)).not.toContain('Some features are unavailable')
|
|
59
|
+
}
|
|
60
|
+
})
|
|
@@ -19,7 +19,11 @@ The base app reads package status and edits the package greeting through
|
|
|
19
19
|
subscriptions; unmount disposes subscriptions and ignores late responses.
|
|
20
20
|
|
|
21
21
|
`--with root-ops` adds the existing DSM approval gate for fixed `root/*.sh`
|
|
22
|
-
operations
|
|
22
|
+
operations, and its `ExecutorGate.vue` holds the pages with the SDK's
|
|
23
|
+
`ExecutorApproval` until the executor is approved: an administrator approves
|
|
24
|
+
with the DSM password, anyone else is told to ask one. A delegate App keeps its
|
|
25
|
+
pages: `AppShell` shows the SDK's `ExecutorNotice` above them while its
|
|
26
|
+
executor waits for approval. `--with jobs` adds package-account jobs and schedules; it combines with root-ops and files. `--with files` enables current-session File Station access,
|
|
23
27
|
and the background picker uses the retained session provider.
|
|
24
28
|
|
|
25
29
|
With `portal:` declared (`synopkgland add portal`, executor Apps only), the status page shows
|
|
@@ -33,14 +37,26 @@ Generated build and unit tests do not establish NAS runtime acceptance.
|
|
|
33
37
|
The manifest is dsm-package.yaml schema 2: backend.kind: go and capabilities.files: session, jobs: true, identity: standalone, rootOps: true. Use --with login to declare standalone identity. Existing projects use synopkgland add <cap>...; app entrypoints stay app-owned, SDK conflicts are refused. check, inspect, build, deploy and status accept --json.
|
|
34
38
|
|
|
35
39
|
Use the installed `@synopackageland/ui`; package.json pins its version.
|
|
36
|
-
`
|
|
37
|
-
`
|
|
38
|
-
|
|
39
|
-
`wide-layout="
|
|
40
|
-
|
|
40
|
+
`App.vue` lays the shell out with the SDK's `AppShell` (`@synopackageland/sdk/vue`):
|
|
41
|
+
`pages` from `shellPages`, the header buttons in `#actions` and the pages in its
|
|
42
|
+
default slot. Given `ctx`, it follows the launch. In a DSM desktop window it
|
|
43
|
+
keeps `wide-layout="window"`, even maximized; the DSM host sets the 700px
|
|
44
|
+
minimum window width. A Portal or standalone launch owns the whole page
|
|
45
|
+
(`ctx.fullPage`, from the generated window): `AppShell` takes
|
|
46
|
+
`wide-layout="web"` (the SaaS layout at 1200px or wider, mobile on a phone) and
|
|
47
|
+
a device-width viewport, undone at unmount, since DSM's page has no
|
|
48
|
+
device-width viewport. Its full-page sidebar names the App with
|
|
49
|
+
`ctx.displayName` and `ctx.appIcon` (without them the page title and icon) and
|
|
50
|
+
shows the signed-in DSM account and its role from `ctx.user`. See the ui's
|
|
51
|
+
`docs/adaptive-layout.md`.
|
|
52
|
+
`AppShell`'s `app-root` wraps it in an app-local `AppRoot` (app modal scope, on
|
|
53
|
+
a full page as well, bounded at `height: 100%`) with the UI locale from
|
|
54
|
+
`locale`; do not add another `AppRoot`.
|
|
55
|
+
In a DSM window (not a full page) that `AppRoot` takes
|
|
41
56
|
`dialog-bounds="viewport"`, so Dialogs can be dragged out of the window, and
|
|
42
|
-
|
|
43
|
-
|
|
57
|
+
pressing one calls `ctx.host.activateWindow()` to bring the window to the
|
|
58
|
+
front. Elsewhere Dialogs stay inside the App. A confirmed sign-in need sets
|
|
59
|
+
`denied`: the `#denied` slot's `LoginPage` takes the place of the App's pages.
|
|
44
60
|
Import `themes/dsm7-embedded.css` and assign each mount a unique
|
|
45
61
|
`app.config.idPrefix` before mounting.
|
|
46
62
|
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
"name": "native-dsm-vue-app",
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@lucide/vue": "1.48.0",
|
|
10
|
-
"@synopackageland/sdk": "0.
|
|
11
|
-
"@synopackageland/ui": "0.9.
|
|
10
|
+
"@synopackageland/sdk": "0.10.1",
|
|
11
|
+
"@synopackageland/ui": "0.9.4",
|
|
12
12
|
"vue": "3.5.43"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
@@ -1148,13 +1148,13 @@
|
|
|
1148
1148
|
}
|
|
1149
1149
|
},
|
|
1150
1150
|
"node_modules/@synopackageland/sdk": {
|
|
1151
|
-
"version": "0.
|
|
1152
|
-
"resolved": "https://registry.npmjs.org/@synopackageland/sdk/-/sdk-0.
|
|
1153
|
-
"integrity": "sha512-
|
|
1151
|
+
"version": "0.10.1",
|
|
1152
|
+
"resolved": "https://registry.npmjs.org/@synopackageland/sdk/-/sdk-0.10.1.tgz",
|
|
1153
|
+
"integrity": "sha512-gs127yFWWYkrNS9lNK/2oHcQ2l0KkhH8dhOAqOScDoGvON212vweVczStdkraesn5Gway+aIMoBqz18uSsD/OA==",
|
|
1154
1154
|
"license": "MIT",
|
|
1155
1155
|
"peerDependencies": {
|
|
1156
1156
|
"@lucide/vue": "^1.48.0",
|
|
1157
|
-
"@synopackageland/ui": "^0.9.
|
|
1157
|
+
"@synopackageland/ui": "^0.9.4",
|
|
1158
1158
|
"vue": "^3.5.0"
|
|
1159
1159
|
},
|
|
1160
1160
|
"peerDependenciesMeta": {
|
|
@@ -1167,9 +1167,9 @@
|
|
|
1167
1167
|
}
|
|
1168
1168
|
},
|
|
1169
1169
|
"node_modules/@synopackageland/ui": {
|
|
1170
|
-
"version": "0.9.
|
|
1171
|
-
"resolved": "https://registry.npmjs.org/@synopackageland/ui/-/ui-0.9.
|
|
1172
|
-
"integrity": "sha512-
|
|
1170
|
+
"version": "0.9.4",
|
|
1171
|
+
"resolved": "https://registry.npmjs.org/@synopackageland/ui/-/ui-0.9.4.tgz",
|
|
1172
|
+
"integrity": "sha512-yu0RrYtT/QcAn/lNwqZBoqqjLTXrpqqnSWB+0swGou8IWLrhfwXrHVBf+oJc+NjLj+G4kB7AyP9kbvw9FHxNOA==",
|
|
1173
1173
|
"license": "MIT",
|
|
1174
1174
|
"dependencies": {
|
|
1175
1175
|
"@internationalized/date": "3.12.4",
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import {computed, onMounted, onUnmounted, ref, watch} from 'vue'
|
|
3
|
-
import {
|
|
4
|
-
import type
|
|
5
|
-
import {AppShell, BackgroundPanel, FilesPage, LoginPage, PortalSettings, createSession, shellPages
|
|
3
|
+
import {Button, Callout, Form, FormActions, FormItem, SettingsSection, Textarea} from '@synopackageland/ui'
|
|
4
|
+
import {bridgeErrorCopy, type AppStatus, type Context, type ShellPage} from '@synopackageland/sdk'
|
|
5
|
+
import {AppShell, BackgroundPanel, FilesPage, LoginPage, PortalSettings, createSession, shellPages} from '@synopackageland/sdk/vue'
|
|
6
6
|
import {appCopy} from './i18n'
|
|
7
7
|
import CapabilityPanels from './CapabilityPanels.vue'
|
|
8
|
+
import ExecutorGate, {holdsPages} from './ExecutorGate.vue'
|
|
8
9
|
const props = defineProps<{ctx: Context}>()
|
|
9
10
|
const login = createSession(props.ctx)
|
|
11
|
+
// Pages ExecutorGate holds need no notice above them that repeats its approval.
|
|
12
|
+
const shellCtx = holdsPages ? {...props.ctx, executor: undefined} : props.ctx
|
|
10
13
|
const copy = computed(() => appCopy(props.ctx.locale))
|
|
11
14
|
const app = ref<AppStatus>()
|
|
12
15
|
const busy = ref(false)
|
|
@@ -17,17 +20,16 @@ let disposed = false
|
|
|
17
20
|
let unsubscribe: (() => void) | undefined
|
|
18
21
|
// The CLI substitutes this only for a declared package-account background worker.
|
|
19
22
|
const backgroundEnabled = (('__BACKGROUND_ENABLED__') as string) === 'true'
|
|
20
|
-
// In a DSM window, dialogs may leave the window as DSM's own do; a press on one
|
|
21
|
-
// does not reach the window, so it brings the window to the front itself.
|
|
22
|
-
const inDsmWindow = props.ctx.host?.platform().host === 'dsm'
|
|
23
|
-
function toFront() {props.ctx.host.activateWindow()}
|
|
24
23
|
const page = ref<ShellPage>('status')
|
|
25
24
|
const pages = computed(() => shellPages({background:backgroundEnabled}))
|
|
25
|
+
// Only a confirmed sign-in need replaces the App's pages; the sign-in page draws its own title.
|
|
26
|
+
const signIn = computed(() => login.ready.value && login.enabled.value && !login.subject.value && {header: false as const})
|
|
26
27
|
const subtitle = computed(() => app.value ? `${app.value.package} · ${app.value.user} · ${app.value.version}` : undefined)
|
|
27
28
|
function message(e: unknown) {
|
|
28
29
|
const code = (e as {code?: string})?.code || 'request_failed'
|
|
29
30
|
const errors = copy.value.errors as Record<string, string>
|
|
30
|
-
|
|
31
|
+
// A bridge App's runtime adds codes (app_unavailable, a service's refusal) the SDK words.
|
|
32
|
+
return bridgeErrorCopy(e, props.ctx.locale) ?? ((code !== 'request_failed' && errors[code]) || errors.request_failed!.replace('{code}', code))
|
|
31
33
|
}
|
|
32
34
|
let again = false
|
|
33
35
|
async function refresh() {
|
|
@@ -76,33 +78,31 @@ onUnmounted(()=>{disposed=true;unsubscribe?.();login.dispose();text.value=''})
|
|
|
76
78
|
</script>
|
|
77
79
|
|
|
78
80
|
<template>
|
|
79
|
-
<
|
|
80
|
-
<
|
|
81
|
-
<
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
<
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
<
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
</SettingsSection>
|
|
98
|
-
</div>
|
|
99
|
-
<CapabilityPanels v-if="app" ref="panels" :key="app.user" :ctx="ctx" :admin="app.admin" />
|
|
100
|
-
<FilesPage v-if="page==='status'" :ctx="ctx" />
|
|
101
|
-
<div data-page="background" v-show="page==='background'">
|
|
102
|
-
<BackgroundPanel v-if="backgroundEnabled && app" ref="background" :key="app.user" :ctx="ctx" />
|
|
103
|
-
</div>
|
|
81
|
+
<AppShell app-root v-model="page" :ctx="shellCtx" :locale="ctx.locale" :pages="pages" :description="subtitle" :denied="signIn" sidebar-storage-key="__PACKAGE__">
|
|
82
|
+
<template #denied><LoginPage :session="login" :locale="ctx.locale" /></template>
|
|
83
|
+
<template #actions><Button variant="secondary" :disabled="busy" @click="refreshAll">{{ copy.refresh }}</Button><Button v-if="login.enabled.value" variant="secondary" :disabled="busy" @click="signOut">{{ copy.signOut }}</Button></template>
|
|
84
|
+
<ExecutorGate :ctx="ctx" :admin="ctx.user?.isAdmin ?? !!app?.admin">
|
|
85
|
+
<div class="integration-app" :aria-busy="busy" :aria-label="copy.settings">
|
|
86
|
+
<Callout v-if="error" tone="danger" :title="copy.actionRequired"><span role="alert">{{ error }}</span></Callout>
|
|
87
|
+
<p v-if="notice" role="status">{{ notice }}</p>
|
|
88
|
+
<div data-page="status" v-show="page==='status'">
|
|
89
|
+
<SettingsSection :title="copy.statusTitle" :collapsible="false" :loading="busy && !app">
|
|
90
|
+
<p v-if="app">{{ app.package }} · {{ app.user }} · {{ app.version }}</p>
|
|
91
|
+
<p v-else-if="error" role="status">{{ copy.statusError }}</p>
|
|
92
|
+
</SettingsSection>
|
|
93
|
+
<PortalSettings v-if="ctx.portal && app" :key="portalKey" :portal="ctx.portal" />
|
|
94
|
+
<SettingsSection v-if="app?.admin" :title="copy.greeting" :collapsible="false">
|
|
95
|
+
<Form direction="vertical" @submit.prevent="save"><FormItem :label="copy.greeting"><Textarea v-model="text" :disabled="busy" /></FormItem>
|
|
96
|
+
<FormActions><Button variant="primary" :disabled="busy || !text.trim()" @click="save">{{ copy.save }}</Button></FormActions>
|
|
97
|
+
</Form>
|
|
98
|
+
</SettingsSection>
|
|
104
99
|
</div>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
100
|
+
<CapabilityPanels v-if="app" ref="panels" :key="app.user" :ctx="ctx" :admin="app.admin" />
|
|
101
|
+
<FilesPage v-if="page==='status'" :ctx="ctx" />
|
|
102
|
+
<div data-page="background" v-show="page==='background'">
|
|
103
|
+
<BackgroundPanel v-if="backgroundEnabled && app" ref="background" :key="app.user" :ctx="ctx" />
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</ExecutorGate>
|
|
107
|
+
</AppShell>
|
|
108
108
|
</template>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// The App's pages, shown at once. The root-ops overlay replaces this file to
|
|
3
|
+
// hold them until an administrator approves the executor.
|
|
4
|
+
export const holdsPages = false
|
|
5
|
+
</script>
|
|
6
|
+
<script setup lang="ts">
|
|
7
|
+
import type {Context} from '@synopackageland/sdk'
|
|
8
|
+
defineProps<{ctx:Context;admin:boolean}>()
|
|
9
|
+
</script>
|
|
10
|
+
<template><slot /></template>
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import {expect,test,vi} from 'vitest'
|
|
3
3
|
import {nextTick} from 'vue'
|
|
4
4
|
import {mount} from '../src/main'
|
|
5
|
-
import type
|
|
5
|
+
import {createStandaloneContext, type Context} from '@synopackageland/sdk'
|
|
6
|
+
import {dsmWindow} from './context'
|
|
6
7
|
async function settle(){for(let i=0;i<16;i++){await Promise.resolve();await nextTick()}}
|
|
7
8
|
function fixture(id:string){
|
|
8
9
|
const reopen:Array<()=>void>=[]
|
|
@@ -12,7 +13,7 @@ function fixture(id:string){
|
|
|
12
13
|
if(method==='greeting.set')return {}
|
|
13
14
|
throw {code:'unknown_method'}
|
|
14
15
|
})
|
|
15
|
-
const ctx={
|
|
16
|
+
const ctx=dsmWindow({call,reopen(fn:()=>void){reopen.push(fn);return ()=>{}}})
|
|
16
17
|
const root=document.createElement('div');document.body.append(root)
|
|
17
18
|
const app=mount(root,ctx)
|
|
18
19
|
return {root,app,call,reopen,ctx,close(){app.unmount();root.remove()}}
|
|
@@ -78,7 +79,7 @@ test('the app renders and loads status while the session check is still pending'
|
|
|
78
79
|
if(method==='greeting.get')return {text:'Hello Eager'}
|
|
79
80
|
throw {code:'unknown_method'}
|
|
80
81
|
})
|
|
81
|
-
const ctx={
|
|
82
|
+
const ctx=dsmWindow({call})
|
|
82
83
|
const root=document.createElement('div');document.body.append(root)
|
|
83
84
|
const app=mount(root,ctx)
|
|
84
85
|
try{
|
|
@@ -99,7 +100,7 @@ test('a status request that raced the session check is retried once the session
|
|
|
99
100
|
if(method==='greeting.get')return {text:'Hello Raced'}
|
|
100
101
|
throw {code:'unknown_method'}
|
|
101
102
|
})
|
|
102
|
-
const ctx={
|
|
103
|
+
const ctx=dsmWindow({call})
|
|
103
104
|
const root=document.createElement('div');document.body.append(root)
|
|
104
105
|
const app=mount(root,ctx)
|
|
105
106
|
try{
|
|
@@ -108,13 +109,14 @@ test('a status request that raced the session check is retried once the session
|
|
|
108
109
|
expect(root.textContent).toContain('Raced · admin · 1')
|
|
109
110
|
}finally{app.unmount();root.remove()}
|
|
110
111
|
})
|
|
111
|
-
function hosted(host
|
|
112
|
+
function hosted(host:'dsm'|'standalone-web'){
|
|
112
113
|
const call=vi.fn(async(method:string)=>{
|
|
113
114
|
if(method==='app.status')return {package:'Hosted',version:'1',user:'admin',admin:false}
|
|
114
115
|
throw {code:'unknown_method'}
|
|
115
116
|
})
|
|
116
|
-
|
|
117
|
-
const ctx=
|
|
117
|
+
// Outside DSM the App owns the page.
|
|
118
|
+
const ctx=host==='dsm'?dsmWindow({call}):createStandaloneContext({call})
|
|
119
|
+
const activateWindow=ctx.host.activateWindow
|
|
118
120
|
const root=document.createElement('div');document.body.append(root)
|
|
119
121
|
const app=mount(root,ctx)
|
|
120
122
|
return {activateWindow,close(){app.unmount();root.remove()}}
|
|
@@ -140,10 +142,8 @@ test('on a narrow viewport the floating dialogs become sheets that stay on scree
|
|
|
140
142
|
}finally{f.close();Object.defineProperty(window,'innerWidth',{configurable:true,value:width})}
|
|
141
143
|
})
|
|
142
144
|
test('outside a DSM window dialogs stay inside the app',async()=>{
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
try{await settle();expect(document.querySelector('[data-ui-floating]')).toBeNull()}finally{f.close()}
|
|
146
|
-
}
|
|
145
|
+
const f=hosted('standalone-web')
|
|
146
|
+
try{await settle();expect(document.querySelector('[data-ui-floating]')).toBeNull()}finally{f.close()}
|
|
147
147
|
})
|
|
148
148
|
test('the status page follows the UI layout rules',async()=>{
|
|
149
149
|
let answer:(value:unknown)=>void=()=>{}
|
|
@@ -153,7 +153,7 @@ test('the status page follows the UI layout rules',async()=>{
|
|
|
153
153
|
throw {code:'unknown_method'}
|
|
154
154
|
})
|
|
155
155
|
const root=document.createElement('div');document.body.append(root)
|
|
156
|
-
const app=mount(root,{
|
|
156
|
+
const app=mount(root,dsmWindow({call}))
|
|
157
157
|
try{
|
|
158
158
|
await settle()
|
|
159
159
|
// A loading section keeps its title and shows a skeleton, not a sentence.
|
|
@@ -166,3 +166,56 @@ test('the status page follows the UI layout rules',async()=>{
|
|
|
166
166
|
expect(root.querySelector('textarea')!.closest('[data-form-item]')!.getAttribute('data-direction')).toBe('vertical')
|
|
167
167
|
}finally{app.unmount();root.remove()}
|
|
168
168
|
})
|
|
169
|
+
test('a code the bridge runtime adds shows the SDK text in the DSM language',async()=>{
|
|
170
|
+
const f=fixture('Bridge')
|
|
171
|
+
try{
|
|
172
|
+
await settle();f.call.mockRejectedValue({code:'app_unavailable'});f.reopen.forEach(fn=>fn());await settle()
|
|
173
|
+
expect(f.root.textContent).toContain('The App is not available right now. Try again later.')
|
|
174
|
+
}finally{f.close()}
|
|
175
|
+
})
|
|
176
|
+
test('a Portal or standalone launch lays the App out full-page; a DSM window keeps the window layout',async()=>{
|
|
177
|
+
// jsdom lays nothing out: give every element a wide desktop width.
|
|
178
|
+
const box=vi.spyOn(HTMLElement.prototype,'getBoundingClientRect').mockReturnValue({width:1400,height:800} as DOMRect)
|
|
179
|
+
const viewport=()=>document.head.querySelector('meta[name="viewport"]')
|
|
180
|
+
const window=fixture('Window')
|
|
181
|
+
const call=vi.fn(async(method:string)=>method==='app.status'?{package:'Page',version:'1',user:'alice',admin:false}:{})
|
|
182
|
+
const ctx=createStandaloneContext({call,displayName:'Page App',appIcon:'webman/3rdparty/Page/images/icon_64.png',user:{name:'alice',isAdmin:false}})
|
|
183
|
+
const root=document.createElement('div');document.body.append(root)
|
|
184
|
+
try{
|
|
185
|
+
await settle()
|
|
186
|
+
expect(window.root.querySelector('[data-ui-layout-mode]')?.getAttribute('data-ui-layout-mode')).toBe('window')
|
|
187
|
+
expect(viewport()).toBeNull()
|
|
188
|
+
// A DSM window has DSM's own title bar and account: the sidebar shows no user.
|
|
189
|
+
expect(window.root.querySelector('[data-ui-sidebar-footer]')).toBeNull()
|
|
190
|
+
const app=mount(root,ctx);await settle()
|
|
191
|
+
expect(root.querySelector('[data-ui-layout-mode]')?.getAttribute('data-ui-layout-mode')).toBe('web')
|
|
192
|
+
// DSM's page has no device-width viewport, so a phone would never reach the mobile layout.
|
|
193
|
+
expect(viewport()?.getAttribute('content')).toContain('width=device-width')
|
|
194
|
+
const brand=root.querySelector('[data-ui-sidebar-brand]')
|
|
195
|
+
expect(brand?.textContent).toContain('Page App')
|
|
196
|
+
expect(brand?.querySelector('img')?.getAttribute('src')).toBe('webman/3rdparty/Page/images/icon_64.png')
|
|
197
|
+
// The signed-in DSM account and its role, from ctx.user.
|
|
198
|
+
expect(root.querySelector('[data-ui-sidebar-footer]')?.textContent).toContain('alice')
|
|
199
|
+
expect(root.querySelector('[data-ui-sidebar-footer]')?.textContent).toContain('DSM user')
|
|
200
|
+
app.unmount()
|
|
201
|
+
expect(viewport()).toBeNull()
|
|
202
|
+
}finally{window.close();root.remove();box.mockRestore()}
|
|
203
|
+
})
|
|
204
|
+
test('a needed sign-in takes the shell page in place of the App pages, inside the App root',async()=>{
|
|
205
|
+
const call=vi.fn(async(method:string)=>{
|
|
206
|
+
if(method==='identity.status')return {enabled:true}
|
|
207
|
+
if(method==='identity.session')return {user:'alice',uid:'1',expiresAt:new Date(Date.now()-1000).toISOString()}
|
|
208
|
+
throw {code:'session_required'}
|
|
209
|
+
})
|
|
210
|
+
const root=document.createElement('div');document.body.append(root)
|
|
211
|
+
const app=mount(root,dsmWindow({call}))
|
|
212
|
+
try{
|
|
213
|
+
await settle()
|
|
214
|
+
const form=root.querySelector('form')
|
|
215
|
+
expect(form).not.toBeNull()
|
|
216
|
+
// The shell lays the sign-in page out as one of its pages: no sidebar, the page body's padding.
|
|
217
|
+
expect(form!.closest('[data-page-content-body]')).not.toBeNull()
|
|
218
|
+
expect(root.querySelector('nav')).toBeNull()
|
|
219
|
+
expect(form!.closest('[data-ui-app]')).not.toBeNull()
|
|
220
|
+
}finally{app.unmount();root.remove()}
|
|
221
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import {vi} from 'vitest'
|
|
2
|
+
import {createStandaloneContext, createStandaloneHost, type Context} from '@synopackageland/sdk'
|
|
3
|
+
/** The ctx the CLI gives a DSM desktop window; activateWindow is a spy. */
|
|
4
|
+
export function dsmWindow(fields: Pick<Context, 'call'> & Partial<Omit<Context, 'call'>>): Context {
|
|
5
|
+
const host = createStandaloneHost({host: 'dsm', dsmStandalone: false})
|
|
6
|
+
vi.spyOn(host, 'activateWindow')
|
|
7
|
+
return createStandaloneContext({fullPage: false, host, ...fields})
|
|
8
|
+
}
|
|
@@ -18,10 +18,9 @@ test('FormItem labels sit above their controls and the UI binds the control id',
|
|
|
18
18
|
expect(items.filter(file=>/<Form(?![^>]*direction="vertical")[\s>]|field\.controlId|#default="\{field\}"/.test(template(file)))).toEqual([])
|
|
19
19
|
})
|
|
20
20
|
// UI built-in text follows the Provider locale (nine locales), so every app root sets it once and no
|
|
21
|
-
// component repeats a per-control locale or download label.
|
|
21
|
+
// component repeats a per-control locale or download label. AppShell's app-root sets it from its locale.
|
|
22
22
|
test('app roots set the UI locale once and components do not translate UI text themselves',()=>{
|
|
23
|
-
|
|
24
|
-
expect(
|
|
25
|
-
expect(roots.filter(file=>!uses(file,'ConfigProvider'))).toEqual([])
|
|
23
|
+
expect(sources(root).filter(file=>/<AppShell[^>]*\sapp-root[\s>]/.test(template(file))).length).toBeGreaterThan(0)
|
|
24
|
+
expect(sources(root).filter(file=>uses(file,'AppRoot')&&!uses(file,'ConfigProvider'))).toEqual([])
|
|
26
25
|
expect(sources(root).filter(file=>/download-label|external-label|<FilePicker[^>]*:locale=/.test(template(file)))).toEqual([])
|
|
27
26
|
})
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
import {expect,test,vi} from 'vitest'
|
|
4
4
|
import {nextTick} from 'vue'
|
|
5
5
|
import {mount} from '../src/main'
|
|
6
|
-
import
|
|
6
|
+
import {dsmWindow} from './context'
|
|
7
7
|
import {appCopy} from '../src/i18n'
|
|
8
8
|
import {identityCopy} from '@synopackageland/sdk/vue'
|
|
9
9
|
async function settle(){for(let i=0;i<20;i++){await Promise.resolve();await nextTick()}}
|
|
10
10
|
const buttons=(root:HTMLElement)=>[...root.querySelectorAll('button')].map(b=>b.textContent?.trim())
|
|
11
11
|
function start(locale:string,call:(method:string)=>Promise<unknown>){
|
|
12
|
-
const ctx={locale,call:vi.fn(call)
|
|
12
|
+
const ctx=dsmWindow({locale,call:vi.fn(call) as never})
|
|
13
13
|
const root=document.createElement('div');document.body.append(root)
|
|
14
14
|
const app=mount(root,ctx)
|
|
15
15
|
return {root,close(){app.unmount();root.remove()}}
|