@things-factory/figure-ui 10.1.146 → 10.1.149

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.
Files changed (209) hide show
  1. package/client/bootstrap.ts +10 -2
  2. package/client/graphql/index.ts +20 -0
  3. package/client/modeller/canvas.ts +6 -2
  4. package/client/modeller/carrier-declaration.ts +63 -0
  5. package/client/modeller/editor.ts +62 -27
  6. package/client/modeller/figure-carrier.ts +117 -0
  7. package/client/modeller/figure-cavity-drawing.ts +3 -9
  8. package/client/modeller/figure-first-save.ts +4 -7
  9. package/client/modeller/figure-history-panel.ts +2 -3
  10. package/client/modeller/figure-links.ts +46 -60
  11. package/client/modeller/figure-material-panel.ts +4 -14
  12. package/client/modeller/figure-mesh-choice.ts +68 -0
  13. package/client/modeller/figure-parts.ts +23 -1
  14. package/client/modeller/figure-preview.ts +2 -2
  15. package/client/modeller/figure-properties.ts +5 -3
  16. package/client/modeller/figure-section-editor.ts +12 -33
  17. package/client/modeller/figure-section-panel.ts +6 -7
  18. package/client/modeller/figure-slot-layout-panel.ts +208 -0
  19. package/client/modeller/figure-verdict.ts +21 -31
  20. package/client/modeller/first-save.ts +23 -0
  21. package/client/modeller/links.ts +52 -1
  22. package/client/modeller/material-edit.ts +38 -0
  23. package/client/modeller/maturity.ts +28 -0
  24. package/client/modeller/mesh-import.ts +77 -0
  25. package/client/modeller/modeller-route.ts +9 -0
  26. package/client/modeller/mounting-part.ts +15 -0
  27. package/client/modeller/newest-answer.ts +21 -0
  28. package/client/modeller/preview-model.ts +5 -0
  29. package/client/modeller/refusals.ts +168 -82
  30. package/client/modeller/section-controls.ts +25 -1
  31. package/client/modeller/section-drawing.ts +43 -1
  32. package/client/modeller/slot-layout-host.ts +140 -0
  33. package/client/pages/figure-modeller-page.ts +6 -5
  34. package/client/viewparts/detail-route.ts +7 -0
  35. package/client/viewparts/figure-detail-dock.ts +1 -3
  36. package/dist-client/bootstrap.d.ts +1 -1
  37. package/dist-client/bootstrap.js +9 -2
  38. package/dist-client/bootstrap.js.map +1 -1
  39. package/dist-client/graphql/index.d.ts +5 -0
  40. package/dist-client/graphql/index.js +19 -0
  41. package/dist-client/graphql/index.js.map +1 -1
  42. package/dist-client/modeller/canvas.d.ts +2 -0
  43. package/dist-client/modeller/canvas.js +10 -2
  44. package/dist-client/modeller/canvas.js.map +1 -1
  45. package/dist-client/modeller/carrier-declaration.d.ts +31 -0
  46. package/dist-client/modeller/carrier-declaration.js +53 -0
  47. package/dist-client/modeller/carrier-declaration.js.map +1 -0
  48. package/dist-client/modeller/editor.d.ts +13 -0
  49. package/dist-client/modeller/editor.js +62 -27
  50. package/dist-client/modeller/editor.js.map +1 -1
  51. package/dist-client/modeller/figure-carrier.d.ts +26 -0
  52. package/dist-client/modeller/figure-carrier.js +123 -0
  53. package/dist-client/modeller/figure-carrier.js.map +1 -0
  54. package/dist-client/modeller/figure-cavity-drawing.js +3 -11
  55. package/dist-client/modeller/figure-cavity-drawing.js.map +1 -1
  56. package/dist-client/modeller/figure-first-save.d.ts +4 -0
  57. package/dist-client/modeller/figure-first-save.js +5 -8
  58. package/dist-client/modeller/figure-first-save.js.map +1 -1
  59. package/dist-client/modeller/figure-history-panel.js +2 -4
  60. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  61. package/dist-client/modeller/figure-links.d.ts +4 -1
  62. package/dist-client/modeller/figure-links.js +47 -57
  63. package/dist-client/modeller/figure-links.js.map +1 -1
  64. package/dist-client/modeller/figure-material-panel.d.ts +0 -1
  65. package/dist-client/modeller/figure-material-panel.js +4 -14
  66. package/dist-client/modeller/figure-material-panel.js.map +1 -1
  67. package/dist-client/modeller/figure-mesh-choice.d.ts +16 -0
  68. package/dist-client/modeller/figure-mesh-choice.js +75 -0
  69. package/dist-client/modeller/figure-mesh-choice.js.map +1 -0
  70. package/dist-client/modeller/figure-parts.d.ts +1 -1
  71. package/dist-client/modeller/figure-parts.js +22 -0
  72. package/dist-client/modeller/figure-parts.js.map +1 -1
  73. package/dist-client/modeller/figure-preview.js +2 -2
  74. package/dist-client/modeller/figure-preview.js.map +1 -1
  75. package/dist-client/modeller/figure-properties.d.ts +2 -1
  76. package/dist-client/modeller/figure-properties.js +5 -3
  77. package/dist-client/modeller/figure-properties.js.map +1 -1
  78. package/dist-client/modeller/figure-section-editor.d.ts +2 -2
  79. package/dist-client/modeller/figure-section-editor.js +12 -33
  80. package/dist-client/modeller/figure-section-editor.js.map +1 -1
  81. package/dist-client/modeller/figure-section-panel.js +6 -7
  82. package/dist-client/modeller/figure-section-panel.js.map +1 -1
  83. package/dist-client/modeller/figure-slot-layout-panel.d.ts +36 -0
  84. package/dist-client/modeller/figure-slot-layout-panel.js +198 -0
  85. package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -0
  86. package/dist-client/modeller/figure-verdict.d.ts +1 -1
  87. package/dist-client/modeller/figure-verdict.js +19 -37
  88. package/dist-client/modeller/figure-verdict.js.map +1 -1
  89. package/dist-client/modeller/first-save.d.ts +10 -0
  90. package/dist-client/modeller/first-save.js +16 -0
  91. package/dist-client/modeller/first-save.js.map +1 -1
  92. package/dist-client/modeller/links.d.ts +26 -1
  93. package/dist-client/modeller/links.js +43 -0
  94. package/dist-client/modeller/links.js.map +1 -1
  95. package/dist-client/modeller/material-edit.d.ts +11 -0
  96. package/dist-client/modeller/material-edit.js +28 -0
  97. package/dist-client/modeller/material-edit.js.map +1 -0
  98. package/dist-client/modeller/maturity.d.ts +15 -0
  99. package/dist-client/modeller/maturity.js +23 -0
  100. package/dist-client/modeller/maturity.js.map +1 -1
  101. package/dist-client/modeller/mesh-import.d.ts +47 -0
  102. package/dist-client/modeller/mesh-import.js +47 -0
  103. package/dist-client/modeller/mesh-import.js.map +1 -0
  104. package/dist-client/modeller/modeller-route.d.ts +5 -0
  105. package/dist-client/modeller/modeller-route.js +9 -0
  106. package/dist-client/modeller/modeller-route.js.map +1 -1
  107. package/dist-client/modeller/mounting-part.d.ts +14 -0
  108. package/dist-client/modeller/mounting-part.js +10 -0
  109. package/dist-client/modeller/mounting-part.js.map +1 -0
  110. package/dist-client/modeller/newest-answer.d.ts +12 -0
  111. package/dist-client/modeller/newest-answer.js +21 -0
  112. package/dist-client/modeller/newest-answer.js.map +1 -0
  113. package/dist-client/modeller/preview-model.d.ts +2 -0
  114. package/dist-client/modeller/preview-model.js +4 -0
  115. package/dist-client/modeller/preview-model.js.map +1 -1
  116. package/dist-client/modeller/refusals.d.ts +26 -5
  117. package/dist-client/modeller/refusals.js +184 -86
  118. package/dist-client/modeller/refusals.js.map +1 -1
  119. package/dist-client/modeller/section-controls.d.ts +17 -1
  120. package/dist-client/modeller/section-controls.js +18 -0
  121. package/dist-client/modeller/section-controls.js.map +1 -1
  122. package/dist-client/modeller/section-drawing.d.ts +24 -1
  123. package/dist-client/modeller/section-drawing.js +42 -0
  124. package/dist-client/modeller/section-drawing.js.map +1 -1
  125. package/dist-client/modeller/slot-layout-host.d.ts +81 -0
  126. package/dist-client/modeller/slot-layout-host.js +105 -0
  127. package/dist-client/modeller/slot-layout-host.js.map +1 -0
  128. package/dist-client/pages/figure-modeller-page.js +5 -4
  129. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  130. package/dist-client/tsconfig.tsbuildinfo +1 -1
  131. package/dist-client/viewparts/detail-route.d.ts +5 -0
  132. package/dist-client/viewparts/detail-route.js +8 -0
  133. package/dist-client/viewparts/detail-route.js.map +1 -0
  134. package/dist-client/viewparts/figure-detail-dock.d.ts +0 -1
  135. package/dist-client/viewparts/figure-detail-dock.js +1 -3
  136. package/dist-client/viewparts/figure-detail-dock.js.map +1 -1
  137. package/package.json +9 -8
  138. package/test/align.test.ts +0 -9
  139. package/test/browser/assistant-stand-in.js +32 -0
  140. package/test/browser/build.mjs +6 -1
  141. package/test/browser/editing-page.js +224 -0
  142. package/test/browser/editing.check.mjs +550 -0
  143. package/test/browser/links-page.js +113 -0
  144. package/test/browser/links.check.mjs +568 -0
  145. package/test/browser/modeller.check.mjs +21 -0
  146. package/test/browser/parts-page.js +120 -0
  147. package/test/browser/parts.check.mjs +328 -0
  148. package/test/browser/places-mesh-page.js +36 -0
  149. package/test/browser/places-mesh.check.mjs +239 -0
  150. package/test/browser/product-page.check.mjs +682 -0
  151. package/test/browser/product-page.js +276 -0
  152. package/test/browser/screen-page.js +112 -0
  153. package/test/browser/screen-product-page.js +62 -0
  154. package/test/browser/screen.check.mjs +1003 -0
  155. package/test/browser/section-page.js +256 -0
  156. package/test/browser/section.check.mjs +719 -0
  157. package/test/browser/serve.mjs +20 -12
  158. package/test/carrier-declaration.test.ts +69 -0
  159. package/test/cavity-drawing.test.ts +2 -18
  160. package/test/clip-ai-proposal.test.ts +1 -5
  161. package/test/clip-consent.test.ts +0 -5
  162. package/test/clips.test.ts +0 -7
  163. package/test/command-preview.test.ts +0 -68
  164. package/test/controls.test.ts +0 -9
  165. package/test/detail-dock.test.ts +6 -25
  166. package/test/face-mapping.test.ts +1 -20
  167. package/test/fasten-drawing.test.ts +0 -23
  168. package/test/fields.test.ts +6 -33
  169. package/test/figure-assistant-surface.test.ts +32 -38
  170. package/test/first-minute.test.ts +3 -55
  171. package/test/first-save-suggest.test.ts +11 -8
  172. package/test/first-save.test.ts +4 -26
  173. package/test/gizmo.test.ts +0 -25
  174. package/test/joint-fan.test.ts +0 -10
  175. package/test/keys.test.ts +0 -7
  176. package/test/layout.test.ts +4 -73
  177. package/test/links.test.ts +48 -67
  178. package/test/live.test.ts +0 -34
  179. package/test/material.test.ts +17 -26
  180. package/test/maturity.test.ts +13 -16
  181. package/test/mesh-import.test.ts +76 -0
  182. package/test/modeller-route.test.ts +3 -19
  183. package/test/package-download.test.ts +6 -7
  184. package/test/parts-wiring.test.ts +9 -51
  185. package/test/preview.test.ts +5 -46
  186. package/test/properties.test.ts +11 -26
  187. package/test/proposal-record.test.ts +2 -19
  188. package/test/recipes.test.ts +0 -15
  189. package/test/refusals.test.ts +257 -151
  190. package/test/release-entry.test.ts +1 -9
  191. package/test/release-panel.test.ts +1 -22
  192. package/test/resize-with-placement.test.ts +0 -32
  193. package/test/revert-choice.test.ts +1 -22
  194. package/test/revert-keeping.test.ts +1 -12
  195. package/test/save-landed.test.ts +16 -14
  196. package/test/scene-canvas.test.ts +2 -13
  197. package/test/section-editor.test.ts +30 -83
  198. package/test/section-panel.test.ts +22 -17
  199. package/test/slot-layout-host.test.ts +117 -0
  200. package/test/theme.test.ts +5 -9
  201. package/test/verdict.test.ts +44 -41
  202. package/test/view.test.ts +5 -12
  203. package/translations/en.json +61 -1
  204. package/translations/es.json +61 -1
  205. package/translations/ja.json +61 -1
  206. package/translations/ko.json +61 -1
  207. package/translations/ms.json +61 -1
  208. package/translations/zh.json +61 -1
  209. package/test/thumbnail.test.ts +0 -29
@@ -0,0 +1,239 @@
1
+ /*
2
+ * A GLB made into a mesh part (ADR-0129) and places made by a rule (ADR-0118), in a real browser, driven as a person
3
+ * does: the palette's import button and the file chooser, the choice of what of the file becomes a part, the places
4
+ * choice, the shared slot layout editor's preset, fields and Apply. Then what the page says and what the session holds
5
+ * are read. The page loads no translations, so the words read are i18n keys. The server that reads a GLB is answered in
6
+ * memory (figure-service tests its reading).
7
+ *
8
+ * Run: node --test test/browser/places-mesh.check.mjs
9
+ */
10
+ import { test, describe, before, after } from 'node:test'
11
+ import assert from 'node:assert/strict'
12
+ import { writeFileSync, mkdtempSync } from 'node:fs'
13
+ import { tmpdir } from 'node:os'
14
+ import { join } from 'node:path'
15
+
16
+ import { addFigureBox, createFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
17
+
18
+ import { buildPage } from './build.mjs'
19
+ import { openPage } from './serve.mjs'
20
+
21
+ const sleep = ms => new Promise(done => setTimeout(done, ms))
22
+
23
+ let opened
24
+ before(async () => {
25
+ opened = await openPage(await buildPage('places-mesh-page.js'))
26
+ })
27
+ after(async () => {
28
+ await opened?.close()
29
+ })
30
+
31
+ const page = () => opened.page
32
+ const read = (fn, ...args) => page().evaluate(fn, ...args)
33
+ const state = () => read(() => probe.state())
34
+ const source = () => read(() => probe.source())
35
+ const drawn = () => read(() => look.drawn())
36
+
37
+ async function until(fn, args, what, ms = 8000) {
38
+ const deadline = Date.now() + ms
39
+ for (;;) {
40
+ if (await read(fn, ...args)) return
41
+ if (Date.now() > deadline) assert.fail(`waited for ${what}`)
42
+ await sleep(50)
43
+ }
44
+ }
45
+ const settled = () => read(() => document.querySelector('figure-editor').updateComplete)
46
+
47
+ async function mount(asset) {
48
+ await read(text => harness.mount('given', text), serializeFigureAsset(asset))
49
+ await until(() => look.drawn().length > 0, [], 'the board is drawn')
50
+ }
51
+ /** The element `findDeep` finds, as a handle the mouse and keyboard act on. */
52
+ const handle = (selector, options = {}) => page().evaluateHandle((s, o) => findDeep(s, o), selector, options)
53
+ async function click(selector, options = {}, what = selector) {
54
+ const element = await handle(selector, options)
55
+ assert.ok(await element.evaluate(e => !!e), `${what} is on the page`)
56
+ await element.evaluate(e => e.scrollIntoView({ block: 'center' }))
57
+ await element.click()
58
+ await settled()
59
+ }
60
+ async function type(selector, options, text) {
61
+ const field = await handle(selector, options)
62
+ assert.ok(await field.evaluate(e => !!e), `${selector} is on the page`)
63
+ await field.evaluate(e => e.scrollIntoView({ block: 'center' }))
64
+ await field.click({ clickCount: 3 })
65
+ await page().keyboard.type(text)
66
+ await page().keyboard.press('Tab')
67
+ await settled()
68
+ }
69
+ async function choose(selector, options, value) {
70
+ const select = await handle(selector, options)
71
+ assert.ok(await select.evaluate(e => !!e), `${selector} is on the page`)
72
+ await select.select(value)
73
+ await settled()
74
+ }
75
+ const has = (selector, options = {}) => read((s, o) => !!findDeep(s, o), selector, options)
76
+ async function pick(part) {
77
+ const at = await read(id => probe.row(id), part)
78
+ assert.ok(at, `the row of ${part}`)
79
+ await page().mouse.click(at.x, at.y)
80
+ await until(id => probe.state().selected === id, [part], `${part} selected`)
81
+ }
82
+
83
+ const PALETTE = { 'palette.dark': '#2b2b2b', 'palette.neutral': '#9a9a9a', 'palette.primary': '#477b9e', 'palette.accent': '#f79c40', 'palette.light': '#dddddd' }
84
+ const figure = name => {
85
+ const a = createFigureAsset(name)
86
+ a.palette = { ...a.palette, ...PALETTE }
87
+ return a
88
+ }
89
+
90
+ describe('a GLB made into a mesh part', () => {
91
+ const HASH = 'c'.repeat(64)
92
+ const bounds = (x0, y0, z0, x1, y1, z1) => ({ min: { x: x0, y: y0, z: z0 }, max: { x: x1, y: y1, z: z1 } })
93
+ const node = (index, name, b, triangles, more = {}) => ({ index, name, parent: index ? 0 : null, bounds: b, triangles, primitives: 1, position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 }, ...more })
94
+ const girder = bounds(-3000, -800, -400, 3000, 200, 400)
95
+ const hoist = bounds(-200, -300, -150, 200, 0, 150)
96
+ const one = { hash: HASH, summary: { bounds: girder, triangles: 5000, primitives: 1, unit: 'm', nodes: [node(0, 'Girder', girder, 5000)] } }
97
+ const crane = {
98
+ hash: HASH,
99
+ summary: {
100
+ bounds: girder,
101
+ triangles: 6200,
102
+ primitives: 2,
103
+ unit: 'm',
104
+ nodes: [node(0, 'Girder', girder, 5000), node(1, 'Hoist Unit #2', hoist, 1200, { position: { x: 1000, y: -500, z: 0 }, rotation: { x: 0, y: 90, z: 0 } })]
105
+ }
106
+ }
107
+ const file = join(mkdtempSync(join(tmpdir(), 'figure-glb-')), 'crane.glb')
108
+ writeFileSync(file, Buffer.from('glTF'))
109
+
110
+ /** The palette's import button, the file chooser it opens, the file chosen. */
111
+ async function importFile() {
112
+ const [chooser] = await Promise.all([page().waitForFileChooser({ timeout: 4000 }), click('button[shape]', { has: 'figure.button.import-mesh' }, 'the import button')])
113
+ await chooser.accept([file])
114
+ await settled()
115
+ }
116
+ const meshNodes = () => read(() => probe.source().document.model.nodes.filter(n => n.op === 'mesh@1'))
117
+
118
+ before(async () => {
119
+ await mount(addFigureBox(figure('Crane'), { id: 'box', size: { x: 400, y: 400, z: 400 }, position: { x: 0, y: 200, z: 0 } }))
120
+ })
121
+
122
+ test('the file chooser takes GLB files', async () => {
123
+ assert.equal(await read(() => findDeep('input[mesh-file]').accept), '.glb,model/gltf-binary')
124
+ })
125
+
126
+ test('a file with one node: uploaded, added at once as one step, named by the file, selected', async () => {
127
+ await read(a => meshServer.answer({ upload: a }), one)
128
+ const start = await state()
129
+ await importFile()
130
+ await until(() => probe.ids().includes('crane'), [], 'the part made of the file')
131
+ assert.deepEqual((await read(() => meshServer.uploads())).at(-1), { name: 'crane.glb', size: 4 })
132
+ const after = await state()
133
+ assert.equal(after.revision, start.revision + 1, 'one step')
134
+ assert.equal(after.selected, 'crane')
135
+ assert.equal(after.error, '')
136
+ const [mesh] = await meshNodes()
137
+ assert.equal(mesh.params.asset, `sha256-${HASH}`)
138
+ assert.equal(await has('figure-mesh-choice'), false, 'nothing to choose between: no question')
139
+ })
140
+
141
+ test('a file of several nodes: the author is asked, and the node chosen is the part, where the file had it', async () => {
142
+ await read(a => meshServer.answer({ upload: a }), crane)
143
+ const start = await state()
144
+ await importFile()
145
+ await until(() => !!findDeep('figure-mesh-choice'), [], 'the question')
146
+ const choices = await read(() => [...findDeep('figure-mesh-choice').shadowRoot.querySelectorAll('li button')].map(b => b.textContent.replace(/\s+/g, ' ').trim()))
147
+ assert.equal(choices.length, 3)
148
+ assert.match(choices[0], /^figure\.text\.mesh-whole-file/)
149
+ assert.match(choices[2], /^Hoist Unit #2/)
150
+ await click('button', { within: 'figure-mesh-choice', has: 'Hoist Unit #2' })
151
+ await until(() => probe.ids().includes('Hoist-Unit-2'), [], 'the hoist made a part')
152
+ const after = await state()
153
+ assert.equal(after.revision, start.revision + 1)
154
+ assert.equal(after.selected, 'Hoist-Unit-2')
155
+ const hoistNode = (await meshNodes()).find(n => n.params.node === 'node-1')
156
+ assert.ok(hoistNode, 'the part is the node, not the whole file')
157
+ })
158
+
159
+ test('the question cancelled: nothing added', async () => {
160
+ await read(a => meshServer.answer({ upload: a }), crane)
161
+ const start = await state()
162
+ await importFile()
163
+ await until(() => !!findDeep('figure-mesh-choice'), [], 'the question')
164
+ await click('md-text-button', { within: 'figure-mesh-choice', text: 'figure.button.cancel' })
165
+ await until(() => !findDeep('figure-mesh-choice'), [], 'the question gone')
166
+ assert.equal((await state()).revision, start.revision)
167
+ })
168
+
169
+ test('a file the server refuses: said in its words, nothing added', async () => {
170
+ await read(() => meshServer.answer({ refused: 'The file is larger than 64 MiB' }))
171
+ const start = await state()
172
+ await importFile()
173
+ await until(() => probe.state().error === 'The file is larger than 64 MiB', [], 'the refusal said')
174
+ assert.equal((await state()).revision, start.revision)
175
+ })
176
+
177
+ test('the app fetches a mesh file from its own server, under the domain it is on', async () => {
178
+ assert.equal(await read(() => meshUrlAt('/domain/acme/figure-modeller/f1')), `/domain/acme/figure-mesh/${HASH}`)
179
+ assert.equal(await read(() => meshUrlAt('/figure-modeller/f1')), `/figure-mesh/${HASH}`)
180
+ })
181
+ })
182
+
183
+ describe('places made by a rule: the shared slot layout editor on a part\'s face', () => {
184
+ before(async () => {
185
+ await mount(addFigureBox(figure('Stacker'), { id: 'fork', size: { x: 1200, y: 50, z: 900 }, position: { x: 0, y: 25, z: 0 } }))
186
+ await pick('fork')
187
+ const tab = await read(l => probe.tab(l), 'figure.label.tab-short-capability')
188
+ await page().mouse.click(tab.x, tab.y)
189
+ await settled()
190
+ })
191
+
192
+ test('one places section, one at a time first; by a rule shows the editor, with no default to fall back to', async () => {
193
+ assert.equal(await has('figure-slot-layout-panel'), false)
194
+ await click('md-filter-chip[data-value="rule"]', {}, 'the by-a-rule choice')
195
+ await until(() => !!findDeep('ox-slot-layout-editor'), [], 'the editor')
196
+ assert.equal(await has('button[data-action="use-default"]'), false, 'no default: the figure\'s layout is the only one')
197
+ })
198
+
199
+ test('a preset on the fork\'s top: a slot that names no type is held back with the figure\'s words', async () => {
200
+ assert.equal(await read(() => findDeep('input[data-preset="width"]').value), '1200', 'the face\'s size is where the preset starts')
201
+ await type('input[data-preset="slotHeight"]', {}, '1000')
202
+ await click('button[data-action="start-preset"]')
203
+ assert.equal(await read(() => findDeep('button[data-action="apply"]').disabled), true)
204
+ assert.equal(await has('*', { within: 'ox-slot-layout-editor', has: 'figure.text.slot-accepts-required' }), true)
205
+ })
206
+
207
+ test('the slot names a type; Apply is one step, the layout the figure holds, its slots drawn on the fork', async () => {
208
+ const start = await state()
209
+ await choose('select[data-field="accepts-mode"]', {}, 'LIST')
210
+ await type('input[data-field="accepts"]', {}, 'PALLET')
211
+ await click('button[data-action="apply"]')
212
+ await until(r => probe.state().revision === r + 1, [start.revision], 'the layout written')
213
+ const held = (await source()).document.capabilities.slotLayout
214
+ assert.equal(held.role, 'seat')
215
+ assert.deepEqual(held.anchors.map(a => [a.id, a.frame]), [['fork', 'fork.face.top.frame']], 'on the fork\'s top')
216
+ assert.deepEqual(held.layout.tiers[0].layout.slot, { allowed: { width: 1200, depth: 900, height: 1000 }, accepts: ['PALLET'] })
217
+ assert.equal(held.layout.preset.key, 'single')
218
+ await until(() => look.drawn().includes('__slot:t1_s1'), [], 'the slot drawn')
219
+ })
220
+
221
+ test('a layout figure-model refuses is said from its details, nothing written', async () => {
222
+ const start = await state()
223
+ const layout = (await source()).document.capabilities.slotLayout.layout
224
+ await read(
225
+ l => look.tell('places', 'command', { action: { kind: 'set-slot-layout', label: 'Forks', role: 'seat', anchors: [{ id: 'a', part: 'fork', face: 'top' }, { id: 'a', part: 'fork', face: 'top' }], layout: l } }),
226
+ layout
227
+ )
228
+ await until(() => probe.state().error === 'figure.text.slot-anchor-duplicate', [], 'the refusal said')
229
+ assert.equal((await state()).revision, start.revision)
230
+ })
231
+
232
+ test('removed: one step, no layout, no slots drawn', async () => {
233
+ const start = await state()
234
+ await click('button', { within: 'figure-slot-layout-panel', has: 'figure.button.remove-slot-layout' })
235
+ await until(r => probe.state().revision === r + 1, [start.revision], 'the layout removed')
236
+ assert.equal((await source()).document.capabilities.slotLayout, undefined)
237
+ assert.equal((await drawn()).some(n => n.startsWith('__slot')), false)
238
+ })
239
+ })