@operato/shell 10.29.1 → 10.29.5

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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@operato/shell",
3
3
  "description": "WebApplication architecturing shell following open-wc recommendations",
4
4
  "author": "heartyoh",
5
- "version": "10.29.1",
5
+ "version": "10.29.5",
6
6
  "type": "module",
7
7
  "main": "dist/src/index.js",
8
8
  "module": "dist/src/index.js",
@@ -60,7 +60,8 @@
60
60
  },
61
61
  "dependencies": {
62
62
  "@material/web": "^2.0.0",
63
- "@operato/graphql": "^10.29.1",
63
+ "@operato/graphql": "^10.29.5",
64
+ "@operato/i18n": "^10.29.1",
64
65
  "@operato/popup": "^10.29.1",
65
66
  "@operato/styles": "^10.29.1",
66
67
  "@operato/utils": "^10.29.1",
@@ -102,5 +103,5 @@
102
103
  "prettier --write"
103
104
  ]
104
105
  },
105
- "gitHead": "1ae246813b79645ee00b600bd7f631bacf1a9213"
106
+ "gitHead": "0995f71d6a9678da116781ec38ae80a253ab8b3e"
106
107
  }
@@ -0,0 +1,153 @@
1
+ import { expect } from '@open-wc/testing'
2
+ import { OxPrompt } from '@operato/popup'
3
+
4
+ import {
5
+ draftDecision,
6
+ draftKey,
7
+ readDraft,
8
+ removeDraftsOf,
9
+ writeDraft,
10
+ DRAFT_LIFETIME_MS,
11
+ UnsavedDraftController,
12
+ type DraftAddress
13
+ } from '../src/index.js'
14
+
15
+ /**
16
+ * Unsaved edits kept as drafts (design/ui/unsaved-drafts.md, architect ruling 2026-09-27): the edit is
17
+ * written as it happens and offered back when the screen opens, so nothing has to ask at reload.
18
+ */
19
+
20
+ const at = (target: string, user = 'u1'): DraftAddress => ({ user, domain: 'd', screen: 'test-modeller', target })
21
+
22
+ class Host {
23
+ controllers: any[] = []
24
+ addController(c: any) {
25
+ this.controllers.push(c)
26
+ }
27
+ removeController() {}
28
+ requestUpdate() {}
29
+ updateComplete = Promise.resolve(true)
30
+ connect() {
31
+ this.controllers.forEach(c => c.hostConnected?.())
32
+ }
33
+ disconnect() {
34
+ this.controllers.forEach(c => c.hostDisconnected?.())
35
+ }
36
+ }
37
+
38
+ describe('draft decision', () => {
39
+ const now = 1_000_000_000_000
40
+
41
+ it('★ a draft whose base is the saved copy now is offered', () => {
42
+ expect(draftDecision({ at: now - 60_000, base: 7 }, 7, now)).to.equal('offer')
43
+ expect(draftDecision({ at: now - 60_000, base: null }, null, now)).to.equal('offer')
44
+ })
45
+
46
+ it('★ saved again since the draft began: offered, and the save will report the conflict', () => {
47
+ expect(draftDecision({ at: now - 60_000, base: 7 }, 8, now)).to.equal('offer-behind')
48
+ expect(draftDecision({ at: now - 60_000, base: null }, 1, now)).to.equal('offer-behind')
49
+ })
50
+
51
+ it('★ older than seven days is dropped without asking', () => {
52
+ expect(draftDecision({ at: now - DRAFT_LIFETIME_MS - 1, base: 7 }, 7, now)).to.equal('drop')
53
+ })
54
+
55
+ it('a key never lets one user read another\'s draft, and survives odd names', () => {
56
+ expect(draftKey(at('x', 'u1'))).to.not.equal(draftKey(at('x', 'u2')))
57
+ expect(draftKey({ user: 'a/b', domain: 'd', screen: 's', target: 't' })).to.not.equal(
58
+ draftKey({ user: 'a', domain: 'b/d', screen: 's', target: 't' })
59
+ )
60
+ })
61
+ })
62
+
63
+ describe('draft store', () => {
64
+ it('★ what is written is read back', async () => {
65
+ const key = draftKey(at('round-trip'))
66
+ await writeDraft({ key, value: { parts: 3 }, base: 7, at: 123 })
67
+ const read = await readDraft<{ parts: number }>(key)
68
+ expect(read?.value.parts).to.equal(3)
69
+ expect(read?.base).to.equal(7)
70
+ })
71
+
72
+ it('★ sign-out takes one user\'s drafts, and only theirs', async () => {
73
+ await writeDraft({ key: draftKey(at('a', 'leaving')), value: 1, base: null, at: 1 })
74
+ await writeDraft({ key: draftKey(at('b', 'leaving')), value: 2, base: null, at: 1 })
75
+ await writeDraft({ key: draftKey(at('a', 'staying')), value: 3, base: null, at: 1 })
76
+
77
+ await removeDraftsOf('leaving')
78
+
79
+ expect(await readDraft(draftKey(at('a', 'leaving')))).to.equal(undefined)
80
+ expect(await readDraft(draftKey(at('b', 'leaving')))).to.equal(undefined)
81
+ expect((await readDraft(draftKey(at('a', 'staying'))))?.value).to.equal(3)
82
+ })
83
+ })
84
+
85
+ describe('UnsavedDraftController', () => {
86
+ const originalOpen = OxPrompt.open
87
+ afterEach(() => ((OxPrompt as any).open = originalOpen))
88
+
89
+ function screen(target: string, state: { value?: number; base: number | null; saved: number | null }) {
90
+ const host = new Host()
91
+ const restored: any[] = []
92
+ const controller = new UnsavedDraftController<number>(host as any, {
93
+ address: () => at(target),
94
+ snapshot: () => (state.value == null ? undefined : { value: state.value, base: state.base }),
95
+ savedRevision: () => state.saved,
96
+ restore: (value, base) => restored.push({ value, base })
97
+ })
98
+ host.connect()
99
+ return { host, controller, restored }
100
+ }
101
+
102
+ it('★ an edit is written; reopening offers it and puts it back with its base', async () => {
103
+ const first = screen('reopen', { value: 42, base: 5, saved: 5 })
104
+ first.controller.changed()
105
+ await first.controller.flush()
106
+ first.host.disconnect()
107
+
108
+ let asked = ''
109
+ ;(OxPrompt as any).open = async (options: any) => ((asked = options.text), true)
110
+ const again = screen('reopen', { base: 5, saved: 5 })
111
+ expect(await again.controller.offer()).to.equal(true)
112
+ expect(again.restored).to.deep.equal([{ value: 42, base: 5 }])
113
+ expect(asked).to.not.contain('conflict')
114
+ })
115
+
116
+ it('★ saved again elsewhere: the question says the save will report the conflict', async () => {
117
+ const first = screen('behind', { value: 1, base: 5, saved: 5 })
118
+ await first.controller.flush()
119
+
120
+ let asked = ''
121
+ ;(OxPrompt as any).open = async (options: any) => ((asked = options.text), true)
122
+ const again = screen('behind', { base: 6, saved: 6 })
123
+ await again.controller.offer()
124
+ expect(asked).to.contain('conflict')
125
+ expect(again.restored[0].base).to.equal(5)
126
+ })
127
+
128
+ it('★ discard removes it; saving removes it', async () => {
129
+ ;(OxPrompt as any).open = async () => false
130
+ const a = screen('discard', { value: 1, base: null, saved: null })
131
+ await a.controller.flush()
132
+ expect(await a.controller.offer()).to.equal(false)
133
+ expect(await readDraft(draftKey(at('discard')))).to.equal(undefined)
134
+
135
+ const b = screen('saved', { value: 1, base: null, saved: null })
136
+ await b.controller.flush()
137
+ await b.controller.settled()
138
+ expect(await readDraft(draftKey(at('saved')))).to.equal(undefined)
139
+ })
140
+
141
+ it('★ where drafts cannot be kept, the browser\'s own question comes back while something is unsaved', async () => {
142
+ const { controller } = screen('fallback', { value: 1, base: null, saved: null })
143
+ ;(controller as any).cannotKeep = true
144
+ const event = new Event('beforeunload', { cancelable: true })
145
+ window.dispatchEvent(event)
146
+ expect(event.defaultPrevented).to.equal(true)
147
+
148
+ ;(controller as any).cannotKeep = false
149
+ const kept = new Event('beforeunload', { cancelable: true })
150
+ window.dispatchEvent(kept)
151
+ expect(kept.defaultPrevented).to.equal(false)
152
+ })
153
+ })