@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.
- package/client/bootstrap.ts +10 -2
- package/client/graphql/index.ts +20 -0
- package/client/modeller/canvas.ts +6 -2
- package/client/modeller/carrier-declaration.ts +63 -0
- package/client/modeller/editor.ts +62 -27
- package/client/modeller/figure-carrier.ts +117 -0
- package/client/modeller/figure-cavity-drawing.ts +3 -9
- package/client/modeller/figure-first-save.ts +4 -7
- package/client/modeller/figure-history-panel.ts +2 -3
- package/client/modeller/figure-links.ts +46 -60
- package/client/modeller/figure-material-panel.ts +4 -14
- package/client/modeller/figure-mesh-choice.ts +68 -0
- package/client/modeller/figure-parts.ts +23 -1
- package/client/modeller/figure-preview.ts +2 -2
- package/client/modeller/figure-properties.ts +5 -3
- package/client/modeller/figure-section-editor.ts +12 -33
- package/client/modeller/figure-section-panel.ts +6 -7
- package/client/modeller/figure-slot-layout-panel.ts +208 -0
- package/client/modeller/figure-verdict.ts +21 -31
- package/client/modeller/first-save.ts +23 -0
- package/client/modeller/links.ts +52 -1
- package/client/modeller/material-edit.ts +38 -0
- package/client/modeller/maturity.ts +28 -0
- package/client/modeller/mesh-import.ts +77 -0
- package/client/modeller/modeller-route.ts +9 -0
- package/client/modeller/mounting-part.ts +15 -0
- package/client/modeller/newest-answer.ts +21 -0
- package/client/modeller/preview-model.ts +5 -0
- package/client/modeller/refusals.ts +168 -82
- package/client/modeller/section-controls.ts +25 -1
- package/client/modeller/section-drawing.ts +43 -1
- package/client/modeller/slot-layout-host.ts +140 -0
- package/client/pages/figure-modeller-page.ts +6 -5
- package/client/viewparts/detail-route.ts +7 -0
- package/client/viewparts/figure-detail-dock.ts +1 -3
- package/dist-client/bootstrap.d.ts +1 -1
- package/dist-client/bootstrap.js +9 -2
- package/dist-client/bootstrap.js.map +1 -1
- package/dist-client/graphql/index.d.ts +5 -0
- package/dist-client/graphql/index.js +19 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/canvas.d.ts +2 -0
- package/dist-client/modeller/canvas.js +10 -2
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/carrier-declaration.d.ts +31 -0
- package/dist-client/modeller/carrier-declaration.js +53 -0
- package/dist-client/modeller/carrier-declaration.js.map +1 -0
- package/dist-client/modeller/editor.d.ts +13 -0
- package/dist-client/modeller/editor.js +62 -27
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-carrier.d.ts +26 -0
- package/dist-client/modeller/figure-carrier.js +123 -0
- package/dist-client/modeller/figure-carrier.js.map +1 -0
- package/dist-client/modeller/figure-cavity-drawing.js +3 -11
- package/dist-client/modeller/figure-cavity-drawing.js.map +1 -1
- package/dist-client/modeller/figure-first-save.d.ts +4 -0
- package/dist-client/modeller/figure-first-save.js +5 -8
- package/dist-client/modeller/figure-first-save.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.js +2 -4
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +4 -1
- package/dist-client/modeller/figure-links.js +47 -57
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-material-panel.d.ts +0 -1
- package/dist-client/modeller/figure-material-panel.js +4 -14
- package/dist-client/modeller/figure-material-panel.js.map +1 -1
- package/dist-client/modeller/figure-mesh-choice.d.ts +16 -0
- package/dist-client/modeller/figure-mesh-choice.js +75 -0
- package/dist-client/modeller/figure-mesh-choice.js.map +1 -0
- package/dist-client/modeller/figure-parts.d.ts +1 -1
- package/dist-client/modeller/figure-parts.js +22 -0
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +2 -2
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-properties.d.ts +2 -1
- package/dist-client/modeller/figure-properties.js +5 -3
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/figure-section-editor.d.ts +2 -2
- package/dist-client/modeller/figure-section-editor.js +12 -33
- package/dist-client/modeller/figure-section-editor.js.map +1 -1
- package/dist-client/modeller/figure-section-panel.js +6 -7
- package/dist-client/modeller/figure-section-panel.js.map +1 -1
- package/dist-client/modeller/figure-slot-layout-panel.d.ts +36 -0
- package/dist-client/modeller/figure-slot-layout-panel.js +198 -0
- package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -0
- package/dist-client/modeller/figure-verdict.d.ts +1 -1
- package/dist-client/modeller/figure-verdict.js +19 -37
- package/dist-client/modeller/figure-verdict.js.map +1 -1
- package/dist-client/modeller/first-save.d.ts +10 -0
- package/dist-client/modeller/first-save.js +16 -0
- package/dist-client/modeller/first-save.js.map +1 -1
- package/dist-client/modeller/links.d.ts +26 -1
- package/dist-client/modeller/links.js +43 -0
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/material-edit.d.ts +11 -0
- package/dist-client/modeller/material-edit.js +28 -0
- package/dist-client/modeller/material-edit.js.map +1 -0
- package/dist-client/modeller/maturity.d.ts +15 -0
- package/dist-client/modeller/maturity.js +23 -0
- package/dist-client/modeller/maturity.js.map +1 -1
- package/dist-client/modeller/mesh-import.d.ts +47 -0
- package/dist-client/modeller/mesh-import.js +47 -0
- package/dist-client/modeller/mesh-import.js.map +1 -0
- package/dist-client/modeller/modeller-route.d.ts +5 -0
- package/dist-client/modeller/modeller-route.js +9 -0
- package/dist-client/modeller/modeller-route.js.map +1 -1
- package/dist-client/modeller/mounting-part.d.ts +14 -0
- package/dist-client/modeller/mounting-part.js +10 -0
- package/dist-client/modeller/mounting-part.js.map +1 -0
- package/dist-client/modeller/newest-answer.d.ts +12 -0
- package/dist-client/modeller/newest-answer.js +21 -0
- package/dist-client/modeller/newest-answer.js.map +1 -0
- package/dist-client/modeller/preview-model.d.ts +2 -0
- package/dist-client/modeller/preview-model.js +4 -0
- package/dist-client/modeller/preview-model.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +26 -5
- package/dist-client/modeller/refusals.js +184 -86
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/section-controls.d.ts +17 -1
- package/dist-client/modeller/section-controls.js +18 -0
- package/dist-client/modeller/section-controls.js.map +1 -1
- package/dist-client/modeller/section-drawing.d.ts +24 -1
- package/dist-client/modeller/section-drawing.js +42 -0
- package/dist-client/modeller/section-drawing.js.map +1 -1
- package/dist-client/modeller/slot-layout-host.d.ts +81 -0
- package/dist-client/modeller/slot-layout-host.js +105 -0
- package/dist-client/modeller/slot-layout-host.js.map +1 -0
- package/dist-client/pages/figure-modeller-page.js +5 -4
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/viewparts/detail-route.d.ts +5 -0
- package/dist-client/viewparts/detail-route.js +8 -0
- package/dist-client/viewparts/detail-route.js.map +1 -0
- package/dist-client/viewparts/figure-detail-dock.d.ts +0 -1
- package/dist-client/viewparts/figure-detail-dock.js +1 -3
- package/dist-client/viewparts/figure-detail-dock.js.map +1 -1
- package/package.json +9 -8
- package/test/align.test.ts +0 -9
- package/test/browser/assistant-stand-in.js +32 -0
- package/test/browser/build.mjs +6 -1
- package/test/browser/editing-page.js +224 -0
- package/test/browser/editing.check.mjs +550 -0
- package/test/browser/links-page.js +113 -0
- package/test/browser/links.check.mjs +568 -0
- package/test/browser/modeller.check.mjs +21 -0
- package/test/browser/parts-page.js +120 -0
- package/test/browser/parts.check.mjs +328 -0
- package/test/browser/places-mesh-page.js +36 -0
- package/test/browser/places-mesh.check.mjs +239 -0
- package/test/browser/product-page.check.mjs +682 -0
- package/test/browser/product-page.js +276 -0
- package/test/browser/screen-page.js +112 -0
- package/test/browser/screen-product-page.js +62 -0
- package/test/browser/screen.check.mjs +1003 -0
- package/test/browser/section-page.js +256 -0
- package/test/browser/section.check.mjs +719 -0
- package/test/browser/serve.mjs +20 -12
- package/test/carrier-declaration.test.ts +69 -0
- package/test/cavity-drawing.test.ts +2 -18
- package/test/clip-ai-proposal.test.ts +1 -5
- package/test/clip-consent.test.ts +0 -5
- package/test/clips.test.ts +0 -7
- package/test/command-preview.test.ts +0 -68
- package/test/controls.test.ts +0 -9
- package/test/detail-dock.test.ts +6 -25
- package/test/face-mapping.test.ts +1 -20
- package/test/fasten-drawing.test.ts +0 -23
- package/test/fields.test.ts +6 -33
- package/test/figure-assistant-surface.test.ts +32 -38
- package/test/first-minute.test.ts +3 -55
- package/test/first-save-suggest.test.ts +11 -8
- package/test/first-save.test.ts +4 -26
- package/test/gizmo.test.ts +0 -25
- package/test/joint-fan.test.ts +0 -10
- package/test/keys.test.ts +0 -7
- package/test/layout.test.ts +4 -73
- package/test/links.test.ts +48 -67
- package/test/live.test.ts +0 -34
- package/test/material.test.ts +17 -26
- package/test/maturity.test.ts +13 -16
- package/test/mesh-import.test.ts +76 -0
- package/test/modeller-route.test.ts +3 -19
- package/test/package-download.test.ts +6 -7
- package/test/parts-wiring.test.ts +9 -51
- package/test/preview.test.ts +5 -46
- package/test/properties.test.ts +11 -26
- package/test/proposal-record.test.ts +2 -19
- package/test/recipes.test.ts +0 -15
- package/test/refusals.test.ts +257 -151
- package/test/release-entry.test.ts +1 -9
- package/test/release-panel.test.ts +1 -22
- package/test/resize-with-placement.test.ts +0 -32
- package/test/revert-choice.test.ts +1 -22
- package/test/revert-keeping.test.ts +1 -12
- package/test/save-landed.test.ts +16 -14
- package/test/scene-canvas.test.ts +2 -13
- package/test/section-editor.test.ts +30 -83
- package/test/section-panel.test.ts +22 -17
- package/test/slot-layout-host.test.ts +117 -0
- package/test/theme.test.ts +5 -9
- package/test/verdict.test.ts +44 -41
- package/test/view.test.ts +5 -12
- package/translations/en.json +61 -1
- package/translations/es.json +61 -1
- package/translations/ja.json +61 -1
- package/translations/ko.json +61 -1
- package/translations/ms.json +61 -1
- package/translations/zh.json +61 -1
- package/test/thumbnail.test.ts +0 -29
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The modeller page itself (`figure-modeller-page`) in a real browser, over a server in memory (product-page.js): what
|
|
3
|
+
* the page does with its editor, its properties, its history panel and the AI dock's door, read as a person or the
|
|
4
|
+
* shell would see it -- a field, a notice, a popup, an address, a request sent.
|
|
5
|
+
*
|
|
6
|
+
* No translations are loaded: a text is its key, with the values put into it after it (product-page.js).
|
|
7
|
+
*
|
|
8
|
+
* Run: node --test test/browser/product-page.check.mjs
|
|
9
|
+
*/
|
|
10
|
+
import { test, describe, before, after } from 'node:test'
|
|
11
|
+
import assert from 'node:assert/strict'
|
|
12
|
+
|
|
13
|
+
import { buildPage } from './build.mjs'
|
|
14
|
+
import { openPage } from './serve.mjs'
|
|
15
|
+
|
|
16
|
+
const STAND_INS = {
|
|
17
|
+
'@things-factory/ai-assistant/dist-client/index.js': 'assistant-stand-in.js',
|
|
18
|
+
'@things-factory/ai-assistant/dist-client/components/assistant-dock.js': 'assistant-stand-in.js'
|
|
19
|
+
}
|
|
20
|
+
const K1 = '11111111-1111-4111-8111-111111111111'
|
|
21
|
+
const K2 = '22222222-2222-4222-8222-222222222222'
|
|
22
|
+
const LID = [{ kind: 'add-part', part: { id: 'lid', kind: 'box', dimensions: { width: 100, height: 10, depth: 100 } } }]
|
|
23
|
+
|
|
24
|
+
let opened
|
|
25
|
+
before(async () => {
|
|
26
|
+
opened = await openPage(await buildPage('product-page.js', { standIns: STAND_INS }))
|
|
27
|
+
})
|
|
28
|
+
after(async () => {
|
|
29
|
+
await opened?.close()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const page = () => opened.page
|
|
33
|
+
const read = (fn, ...args) => page().evaluate(fn, ...args)
|
|
34
|
+
/** Waits for a condition in the page (no fixed sleep): its value once truthy. */
|
|
35
|
+
async function until(fn, label, ...args) {
|
|
36
|
+
try {
|
|
37
|
+
return await (await page().waitForFunction(fn, { timeout: 15000, polling: 50 }, ...args)).jsonValue()
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw Error(`waited in vain: ${label} (${error.message})`)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** A fresh page: nothing seeded, nothing asked, nothing open. */
|
|
43
|
+
async function fresh() {
|
|
44
|
+
/* Taken down first: a page with unsaved edits holds a reload with the browser's own question. */
|
|
45
|
+
await read(() => document.querySelector('figure-modeller-page')?.remove())
|
|
46
|
+
await page().reload()
|
|
47
|
+
await page().waitForFunction(() => !!window.product, { timeout: 30000 })
|
|
48
|
+
}
|
|
49
|
+
/** The page made active on an address, and the figure on it read. */
|
|
50
|
+
async function open(id) {
|
|
51
|
+
await read(id => product.open(id ?? undefined), id ?? null)
|
|
52
|
+
await until(() => { const v = product.view(); return v && v.context && !product.page().session.read().loading }, `figure ${id ?? 'new'} open`)
|
|
53
|
+
}
|
|
54
|
+
/** Clicks the first element the selector finds anywhere on the page, through shadow roots. */
|
|
55
|
+
async function press(selector, label = selector) {
|
|
56
|
+
await until(s => { const e = product.deep(s)[0]; return !!e && !e.disabled }, `${label} to press`, selector)
|
|
57
|
+
await read(s => product.deep(s)[0].click(), selector)
|
|
58
|
+
}
|
|
59
|
+
/** A field of the properties tab changed by the author: its value set and the change told. */
|
|
60
|
+
async function change(selector, value) {
|
|
61
|
+
await until(s => !!product.deep(s)[0], selector, selector)
|
|
62
|
+
await read((s, v) => { const input = product.deep(s)[0]; input.value = v; input.dispatchEvent(new Event('change', { bubbles: true })) }, selector, value)
|
|
63
|
+
}
|
|
64
|
+
const rename = name => change('figure-properties >>> div[field] input', name)
|
|
65
|
+
const fields = () => read(() => product.calls().map(c => c.field))
|
|
66
|
+
const callsTo = field => read(f => product.calls().filter(c => c.field === f).map(c => c.variables), field)
|
|
67
|
+
const notices = () => read(() => product.notices())
|
|
68
|
+
const releaseState = () => read(() => product.deep('[release-state]')[0]?.textContent.trim())
|
|
69
|
+
|
|
70
|
+
describe('a save whose answer never came back', () => {
|
|
71
|
+
before(async () => {
|
|
72
|
+
await fresh()
|
|
73
|
+
await read(() => product.seed({ id: 'a', type: 'agv', source: product.boxSource('A') }))
|
|
74
|
+
await open('a')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
/* The save goes through `reached`, and the store reads every field it wrote back from the server. */
|
|
78
|
+
test('written, its answer lost: read back from the server, it is saved -- no notice, nothing left unsaved', async () => {
|
|
79
|
+
await rename('B')
|
|
80
|
+
await read(() => product.answer('saveFigure', v => {
|
|
81
|
+
product.server('saveFigure', v)
|
|
82
|
+
throw Object.assign(Error('lost'), { networkError: {} })
|
|
83
|
+
}))
|
|
84
|
+
await press('button[save]', 'save')
|
|
85
|
+
await until(() => !product.view().dirty, 'the save read back as saved')
|
|
86
|
+
assert.deepEqual(await notices(), [])
|
|
87
|
+
assert.equal(JSON.parse((await read(() => product.rows()))[0].source).name, 'B')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('not written and not readable back: said as not known, and the edits stay', async () => {
|
|
91
|
+
await read(() => product.forget())
|
|
92
|
+
await rename('C')
|
|
93
|
+
await read(() => {
|
|
94
|
+
const lost = () => { throw Object.assign(Error('lost'), { networkError: {} }) }
|
|
95
|
+
product.answer('saveFigure', lost)
|
|
96
|
+
product.answer('figure', lost)
|
|
97
|
+
})
|
|
98
|
+
await press('button[save]', 'save')
|
|
99
|
+
const said = await until(() => product.notices().find(n => n.level === 'error'), 'a notice')
|
|
100
|
+
assert.equal(said.message, 'figure.text.save-unreachable')
|
|
101
|
+
assert.equal((await read(() => product.view())).dirty, true)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('a released figure the server makes a draft again', () => {
|
|
106
|
+
before(async () => {
|
|
107
|
+
await fresh()
|
|
108
|
+
await read(() => {
|
|
109
|
+
product.seed({ id: 'r', type: 'agv', state: 'released', version: 2, source: product.boxSource('R') })
|
|
110
|
+
product.seed({ id: 'c', type: 'oht', state: 'released', version: 1, source: product.boxSource('C') })
|
|
111
|
+
product.seed({ id: 'n', type: 'stk', state: 'draft', version: 0, source: product.boxSource('N') })
|
|
112
|
+
product.seed({ id: 'o', type: 'lft', state: 'draft', version: 3, source: product.boxSource('O') })
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('beside the save: what the saved figure is, as the row on the server says', async () => {
|
|
117
|
+
await open('n')
|
|
118
|
+
await until(() => product.deep('[release-state]')[0]?.textContent.trim(), 'the release state')
|
|
119
|
+
assert.equal(await releaseState(), 'figure.text.release-state-draft-new')
|
|
120
|
+
await open('o')
|
|
121
|
+
await until(() => product.deep('[release-state]')[0]?.textContent.includes('draft-over'), 'the release state of o')
|
|
122
|
+
assert.equal(await releaseState(), 'figure.text.release-state-draft-over {"version":"3"}')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('its source saved: told once, and the state beside the save follows', async () => {
|
|
126
|
+
await open('r')
|
|
127
|
+
await until(() => product.deep('[release-state]')[0]?.textContent.includes('released'), 'released')
|
|
128
|
+
assert.equal(await releaseState(), 'figure.text.release-state-released {"version":"2"}')
|
|
129
|
+
await read(() => product.forget())
|
|
130
|
+
await rename('R2')
|
|
131
|
+
await press('button[save]', 'save')
|
|
132
|
+
await until(() => product.deep('[release-state]')[0]?.textContent.includes('draft-over'), 'the state follows')
|
|
133
|
+
assert.deepEqual(await notices(), [{ level: 'info', message: 'figure.text.released-now-draft' }])
|
|
134
|
+
assert.equal(await releaseState(), 'figure.text.release-state-draft-over {"version":"2"}')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('its category changed: written to the row at once, told, and the state follows', async () => {
|
|
138
|
+
await open('c')
|
|
139
|
+
await until(() => product.deep('[release-state]')[0]?.textContent.includes('released'), 'released')
|
|
140
|
+
await read(() => {
|
|
141
|
+
product.forget()
|
|
142
|
+
product.answer('updateFigure', v => {
|
|
143
|
+
const answer = product.server('updateFigure', v)
|
|
144
|
+
answer.figure.state = 'draft'
|
|
145
|
+
return answer
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
const before = await read(() => product.view())
|
|
149
|
+
await change('figure-properties >>> input[placeholder="figure.text.category-placeholder"]', 'conveyors')
|
|
150
|
+
await until(() => product.deep('[release-state]')[0]?.textContent.includes('draft-over'), 'the state follows')
|
|
151
|
+
assert.deepEqual(await callsTo('updateFigure'), [{ id: 'c', patch: { category: 'conveyors' } }])
|
|
152
|
+
assert.deepEqual(await notices(), [{ level: 'info', message: 'figure.text.released-now-draft' }])
|
|
153
|
+
const now = await read(() => product.view())
|
|
154
|
+
assert.equal(now.source, before.source, 'the row, not the figure')
|
|
155
|
+
assert.equal(now.dirty, false)
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
describe('the first save of a new figure', () => {
|
|
160
|
+
before(async () => {
|
|
161
|
+
await fresh()
|
|
162
|
+
await read(() => product.seed({ id: 'old', type: 'taken-one', source: product.boxSource('Old') }))
|
|
163
|
+
await open(null)
|
|
164
|
+
})
|
|
165
|
+
const firstSave = () =>
|
|
166
|
+
read(() => {
|
|
167
|
+
const popup = product.deep('figure-first-save')[0]
|
|
168
|
+
if (!popup) return null
|
|
169
|
+
let active = document.activeElement
|
|
170
|
+
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement
|
|
171
|
+
return { first: popup.shadowRoot.querySelector('input')?.id, type: popup.shadowRoot.querySelector('#type').value, focused: active?.id }
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test('before it is saved: the type can be written, held by the page, and the release waits', async () => {
|
|
175
|
+
const field = await read(() => { const input = product.page().shadowRoot.querySelector('label[type-field] input'); return input && { disabled: input.disabled } })
|
|
176
|
+
assert.deepEqual(field, { disabled: false })
|
|
177
|
+
assert.equal(await read(() => product.deep('button[publish]')[0].disabled), true, 'nothing to release before the first save')
|
|
178
|
+
await read(() => product.forget())
|
|
179
|
+
await change('figure-modeller-page >>> label[type-field] input', 'agv lifter')
|
|
180
|
+
assert.equal(await read(() => product.page().shadowRoot.querySelector('label[type-field] input').value), 'agv-lifter')
|
|
181
|
+
assert.equal((await fields()).includes('updateFigure'), false, 'no row to write yet')
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('the question asks the name first, focused, with the type written before', async () => {
|
|
185
|
+
await press('button[save]', 'save')
|
|
186
|
+
await until(() => product.deep('figure-first-save')[0]?.shadowRoot.querySelector('#type'), 'the first save asked')
|
|
187
|
+
await until(() => { let a = document.activeElement; while (a?.shadowRoot?.activeElement) a = a.shadowRoot.activeElement; return a?.id === 'name' }, 'the name focused')
|
|
188
|
+
assert.deepEqual(await firstSave(), { first: 'name', type: 'agv-lifter', focused: 'name' })
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('a first save refused opens again on the identifier answered; typing a name does not change it', async () => {
|
|
192
|
+
await read(() => {
|
|
193
|
+
const type = product.deep('figure-first-save')[0].shadowRoot.querySelector('#type')
|
|
194
|
+
type.value = 'my lift'
|
|
195
|
+
type.dispatchEvent(new Event('input'))
|
|
196
|
+
product.answer('saveFigure', () => {
|
|
197
|
+
product.answer('saveFigure')
|
|
198
|
+
throw Object.assign(Error('refused'), { graphQLErrors: [{ message: 'refused', extensions: { code: 'FORBIDDEN' } }] })
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
await read(() => product.deep('figure-first-save')[0].shadowRoot.querySelector('md-filled-button[slot=confirm]').click())
|
|
202
|
+
await until(() => product.notices().some(n => n.level === 'error'), 'the refusal said')
|
|
203
|
+
await until(() => !product.deep('figure-first-save').length, 'the question closed')
|
|
204
|
+
await press('button[save]', 'save')
|
|
205
|
+
await until(() => product.deep('figure-first-save')[0]?.shadowRoot.querySelector('#type'), 'asked again')
|
|
206
|
+
assert.equal((await firstSave()).type, 'my-lift')
|
|
207
|
+
await read(() => {
|
|
208
|
+
const name = product.deep('figure-first-save >>> #name')[0]
|
|
209
|
+
name.focus()
|
|
210
|
+
name.select()
|
|
211
|
+
})
|
|
212
|
+
await page().keyboard.type('Lift table')
|
|
213
|
+
await until(() => product.deep('figure-first-save')[0].shadowRoot.querySelector('#name').value === 'Lift table', 'the name typed')
|
|
214
|
+
assert.equal((await firstSave()).type, 'my-lift', 'the name typed: the identifier stays')
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test("saved: the address becomes the figure's own in place of the entry; the bare address opens a new figure", async () => {
|
|
218
|
+
const entries = await read(() => history.length)
|
|
219
|
+
await read(() => product.forget())
|
|
220
|
+
await read(() => product.deep('figure-first-save')[0].shadowRoot.querySelector('md-filled-button[slot=confirm]').click())
|
|
221
|
+
const id = await until(() => product.view().id, 'saved')
|
|
222
|
+
assert.equal((await callsTo('saveFigure'))[0].type, 'my-lift')
|
|
223
|
+
const navigations = await until(() => product.navigations().length && product.navigations(), 'the address changed')
|
|
224
|
+
assert.deepEqual(navigations, [{ path: `/figure-modeller/${id}`, entries, detailShown: false }])
|
|
225
|
+
await until(() => !product.deep('button[publish]')[0].disabled, 'the release open')
|
|
226
|
+
assert.equal((await read(() => product.view())).id, id, 'the page stays on it')
|
|
227
|
+
await open(null)
|
|
228
|
+
assert.equal((await read(() => product.view())).id, undefined, 'the modeller from the menu: a new figure')
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
describe('opening another figure', () => {
|
|
233
|
+
before(async () => {
|
|
234
|
+
await fresh()
|
|
235
|
+
await read(() => {
|
|
236
|
+
product.seed({ id: 'a', type: 'type-a', source: product.boxSource('A') })
|
|
237
|
+
product.seed({ id: 'b', type: 'type-b', source: product.boxSource('B') })
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('the row is emptied first: the type of the one before does not stand while the next one loads', async () => {
|
|
242
|
+
const typeField = () => read(() => product.page().shadowRoot.querySelector('label[type-field] input')?.value)
|
|
243
|
+
await open('a')
|
|
244
|
+
await until(() => product.page().shadowRoot.querySelector('label[type-field] input')?.value === 'type-a', 'a\'s type')
|
|
245
|
+
/* Only b's first read is held: the figure itself. */
|
|
246
|
+
await read(() =>
|
|
247
|
+
product.answer('figure', v => {
|
|
248
|
+
if (v.id !== 'b') return product.server('figure', v)
|
|
249
|
+
product.answer('figure')
|
|
250
|
+
return new Promise(done => (window.letB = () => done(product.server('figure', v))))
|
|
251
|
+
})
|
|
252
|
+
)
|
|
253
|
+
await read(() => product.open('b'))
|
|
254
|
+
await until(() => !!window.letB, 'b asked for')
|
|
255
|
+
await read(() => product.page().updateComplete)
|
|
256
|
+
assert.equal(await typeField(), '')
|
|
257
|
+
await read(() => window.letB())
|
|
258
|
+
await until(() => product.page().shadowRoot.querySelector('label[type-field] input')?.value === 'type-b', 'b\'s type')
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
const PANEL = 'figure-modeller-page >>> figure-history-panel'
|
|
263
|
+
const TAB = name => `figure-editor >>> button[tab][aria-label="figure.label.tab-short-${name}"]`
|
|
264
|
+
const VERSION_1 = { version: 1, comment: 'first', state: 'released', updatedAt: null, updater: null, score: null, triangles: null, groups: null }
|
|
265
|
+
/** The revert choice popup's buttons: keep editing, let the edits go, save them first. */
|
|
266
|
+
const CHOICE = { cancel: 'figure-revert-choice >>> md-text-button[slot=cancel]', discard: 'figure-revert-choice >>> md-text-button[slot=start]', save: 'figure-revert-choice >>> md-filled-button[slot=confirm]' }
|
|
267
|
+
const choiceShown = () => until(() => product.deep('figure-revert-choice >>> p')[0]?.textContent.trim(), 'the revert choice')
|
|
268
|
+
const choiceGone = () => until(() => !product.deep('figure-revert-choice').length, 'the revert choice closed')
|
|
269
|
+
const failure = panel => read(p => product.deep(`${p} >>> p[failed]`)[0]?.textContent.trim(), panel)
|
|
270
|
+
|
|
271
|
+
describe('the history panel on the page', () => {
|
|
272
|
+
before(async () => {
|
|
273
|
+
await fresh()
|
|
274
|
+
await read(VERSION_1 => {
|
|
275
|
+
product.seed({ id: 'h', type: 'agv', namespace: 'acme', state: 'draft', version: 1, source: product.boxSource('H') })
|
|
276
|
+
product.answer('figureVersions', () => [VERSION_1])
|
|
277
|
+
product.answer('inspectFigure', () => ({
|
|
278
|
+
blocked: true,
|
|
279
|
+
findings: ['occupancy-undeclared', 'part-outside-occupancy', 'type-is-placeholder'].map(code => ({ code, message: code, why: null, how: null, at: null, blocking: true }))
|
|
280
|
+
}))
|
|
281
|
+
}, VERSION_1)
|
|
282
|
+
await open('h')
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test('the release beside the save leads to the release section of the history tab', async () => {
|
|
286
|
+
await press('button[publish]', 'release')
|
|
287
|
+
await until(p => product.deep(p)[0]?.shadowRoot.activeElement?.closest('section[release]'), 'the release section focused', PANEL)
|
|
288
|
+
const panel = await read(p => { const panel = product.deep(p)[0]; return { slot: panel.assignedSlot?.name, visible: panel.checkVisibility() } }, PANEL)
|
|
289
|
+
assert.deepEqual(panel, { slot: 'history', visible: true })
|
|
290
|
+
assert.equal(await read(() => product.deep('figure-editor >>> button[tab][on]')[0]?.getAttribute('aria-label')), 'figure.label.tab-short-history')
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
test('a released version downloads as a package by its full reference', async () => {
|
|
294
|
+
const link = await until(p => { const a = product.deep(`${p} >>> li a[package]`)[0]; return a && { href: a.getAttribute('href'), download: a.hasAttribute('download') } }, 'the package link', PANEL)
|
|
295
|
+
assert.deepEqual(link, { href: '/figure-package?refs=acme%2Fagv%401', download: true })
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
test('fitting to the parts is offered for the two findings it answers, and declares the room as one step', async () => {
|
|
299
|
+
const offered = await until(p => { const found = product.deep(`${p} >>> li[finding]`); return found.length === 3 && found.filter(li => li.querySelector('button[fit-base]')).map(li => li.querySelector('[msg]').textContent).sort() }, 'the findings', PANEL)
|
|
300
|
+
assert.deepEqual(offered, ['occupancy-undeclared', 'part-outside-occupancy'])
|
|
301
|
+
const before = await read(() => product.view())
|
|
302
|
+
await press(`${PANEL} >>> button[fit-base]`, 'fit')
|
|
303
|
+
await until(r => product.view().revision === r + 1, 'one step', before.revision)
|
|
304
|
+
await press(TAB('properties'), 'the properties tab')
|
|
305
|
+
await until(() => product.deep('figure-properties >>> div[row] > span')[0]?.textContent === 'figure.text.declared-space-declared', 'the room declared')
|
|
306
|
+
await press(TAB('history'), 'the history tab')
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
test('with edits not saved, a revert asks first; kept editing, nothing is sent', async () => {
|
|
310
|
+
assert.equal((await read(() => product.view())).dirty, true)
|
|
311
|
+
await read(() => product.forget())
|
|
312
|
+
await press(`${PANEL} >>> li button[revert]:not([undo])`, 'revert v1')
|
|
313
|
+
assert.equal(await choiceShown(), 'figure.text.revert-with-unsaved {"version":"1"}')
|
|
314
|
+
assert.equal(await read(s => product.deep(s)[0].hasAttribute('destructive'), CHOICE.discard), true, 'letting the edits go is marked as what it is')
|
|
315
|
+
await press(CHOICE.cancel, 'keep editing')
|
|
316
|
+
await choiceGone()
|
|
317
|
+
assert.equal((await fields()).includes('revertFigureVersion'), false)
|
|
318
|
+
assert.equal(await failure(PANEL), undefined)
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
test('a save first that is refused, or not known, holds the revert', async () => {
|
|
322
|
+
await read(() => {
|
|
323
|
+
product.forget()
|
|
324
|
+
product.answer('saveFigure', () => {
|
|
325
|
+
throw Object.assign(Error('refused'), { graphQLErrors: [{ message: 'refused', extensions: { code: 'FORBIDDEN' } }] })
|
|
326
|
+
})
|
|
327
|
+
})
|
|
328
|
+
await press(`${PANEL} >>> li button[revert]:not([undo])`, 'revert v1')
|
|
329
|
+
await choiceShown()
|
|
330
|
+
await press(CHOICE.save, 'save first')
|
|
331
|
+
const held = await until(p => product.deep(`${p} >>> p[failed]`)[0]?.textContent.trim(), 'the revert held', PANEL)
|
|
332
|
+
assert.equal(held, 'figure.text.revert-held-unsaved {"reason":"refused"}')
|
|
333
|
+
await read(() => {
|
|
334
|
+
const lost = () => { throw Object.assign(Error('lost'), { networkError: {} }) }
|
|
335
|
+
product.answer('saveFigure', lost)
|
|
336
|
+
product.answer('figure', lost)
|
|
337
|
+
})
|
|
338
|
+
await press(`${PANEL} >>> li button[revert]:not([undo])`, 'revert v1')
|
|
339
|
+
await choiceShown()
|
|
340
|
+
await press(CHOICE.save, 'save first')
|
|
341
|
+
await until(p => product.deep(`${p} >>> p[failed]`)[0]?.textContent.includes('save-unreachable'), 'not known', PANEL)
|
|
342
|
+
assert.equal(await failure(PANEL), 'figure.text.revert-held-unsaved {"reason":"figure.text.save-unreachable"}')
|
|
343
|
+
assert.equal((await fields()).includes('revertFigureVersion'), false)
|
|
344
|
+
assert.equal((await read(() => product.view())).dirty, true, 'the edits stay')
|
|
345
|
+
await read(() => {
|
|
346
|
+
product.answer('saveFigure')
|
|
347
|
+
product.answer('figure')
|
|
348
|
+
})
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
test('edits let go: the revert expects what the server holds, and an answer lost is asked again with the same request', async () => {
|
|
352
|
+
await read(() => {
|
|
353
|
+
product.forget()
|
|
354
|
+
product.answer('revertFigureVersion', () => { throw Object.assign(Error('lost'), { networkError: {} }) })
|
|
355
|
+
})
|
|
356
|
+
await press(`${PANEL} >>> li button[revert]:not([undo])`, 'revert v1')
|
|
357
|
+
await choiceShown()
|
|
358
|
+
await press(CHOICE.discard, 'let the edits go')
|
|
359
|
+
await until(p => product.deep(`${p} >>> p[failed]`)[0]?.textContent.trim() === 'figure.text.revert-unconfirmed', 'not known whether it was reverted', PANEL)
|
|
360
|
+
const [first] = await callsTo('revertFigureVersion')
|
|
361
|
+
const held = (await read(() => product.rows())).find(r => r.id === 'h').source
|
|
362
|
+
assert.deepEqual({ ...first, requestId: typeof first.requestId }, { id: 'h', version: 1, requestId: 'string', expectedSource: held })
|
|
363
|
+
assert.notEqual(first.expectedSource, (await read(() => product.view())).source, 'not the edits on screen')
|
|
364
|
+
|
|
365
|
+
await read(() => {
|
|
366
|
+
product.answer('revertFigureVersion', v => {
|
|
367
|
+
product.seed({ ...product.rows().find(r => r.id === v.id), source: product.boxSource('Released v1') })
|
|
368
|
+
const kept = { id: 'k1', kind: 'revert', state: 'draft', version: 1, revertedToVersion: v.version, restoredAt: null, createdAt: null, creator: null }
|
|
369
|
+
product.answer('figureReplacedDrafts', () => [kept])
|
|
370
|
+
return { figure: product.server('figure', { id: v.id }), violations: [], kept }
|
|
371
|
+
})
|
|
372
|
+
})
|
|
373
|
+
await press(`${PANEL} >>> li button[revert]:not([undo])`, 'revert v1 again')
|
|
374
|
+
await choiceShown()
|
|
375
|
+
await press(CHOICE.discard, 'let the edits go')
|
|
376
|
+
await until(() => product.view().name === 'Released v1' && !product.view().dirty, 'the figure reopened as reverted')
|
|
377
|
+
const [, again] = await callsTo('revertFigureVersion')
|
|
378
|
+
assert.equal(again.requestId, first.requestId, 'the same revert, asked again')
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
test('the draft a revert wrote over is listed apart from the releases, and can be taken back', async () => {
|
|
382
|
+
const lists = await until(p => {
|
|
383
|
+
const kept = product.deep(`${p} >>> section[kept] li`)
|
|
384
|
+
return kept.length && { kept: kept.length, undo: kept.filter(li => li.querySelector('button[undo]')).length, released: product.deep(`${p} >>> section:not([kept]):not([release]) li`).length }
|
|
385
|
+
}, 'the kept list', PANEL)
|
|
386
|
+
assert.deepEqual(lists, { kept: 1, undo: 1, released: 1 })
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
test('edits saved first: the revert expects what that save wrote', async () => {
|
|
390
|
+
await rename('Edited')
|
|
391
|
+
await read(() => {
|
|
392
|
+
product.forget()
|
|
393
|
+
product.answer('inspectFigure', () => ({ blocked: false, findings: [] }))
|
|
394
|
+
})
|
|
395
|
+
await press(`${PANEL} >>> li button[revert]:not([undo])`, 'revert v1')
|
|
396
|
+
await choiceShown()
|
|
397
|
+
await press(CHOICE.save, 'save first')
|
|
398
|
+
await until(() => product.calls().some(c => c.field === 'revertFigureVersion'), 'the revert sent')
|
|
399
|
+
const sent = await fields()
|
|
400
|
+
assert.ok(sent.indexOf('saveFigure') >= 0 && sent.indexOf('saveFigure') < sent.indexOf('revertFigureVersion'), sent.join(' '))
|
|
401
|
+
const [saved] = await callsTo('saveFigure')
|
|
402
|
+
const [reverted] = await callsTo('revertFigureVersion')
|
|
403
|
+
assert.equal(JSON.parse(saved.source).name, 'Edited')
|
|
404
|
+
assert.equal(reverted.expectedSource, saved.source)
|
|
405
|
+
await until(() => !product.view().dirty, 'reopened')
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
test('released from the panel: the state beside the save follows, and the locked type stays', async () => {
|
|
409
|
+
await press(`${PANEL} >>> button[release]`, 'release')
|
|
410
|
+
await until(() => product.deep('[release-state]')[0]?.textContent.includes('released'), 'released')
|
|
411
|
+
assert.equal(await releaseState(), 'figure.text.release-state-released {"version":"2"}')
|
|
412
|
+
assert.equal(await read(() => product.page().shadowRoot.querySelector('label[type-field] code')?.textContent), 'agv')
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
test('taking a revert back with edits not saved asks too, in its own words', async () => {
|
|
416
|
+
await rename('Edited again')
|
|
417
|
+
await read(() => product.forget())
|
|
418
|
+
await press(`${PANEL} >>> section[kept] button[undo]`, 'undo the revert')
|
|
419
|
+
assert.equal(await choiceShown(), 'figure.text.undo-revert-with-unsaved')
|
|
420
|
+
await press(CHOICE.cancel, 'keep editing')
|
|
421
|
+
await choiceGone()
|
|
422
|
+
assert.equal((await fields()).includes('undoFigureRevert'), false)
|
|
423
|
+
})
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
describe('a history panel of its own', () => {
|
|
427
|
+
before(async () => {
|
|
428
|
+
await fresh()
|
|
429
|
+
await read(VERSION_1 => product.answer('figureVersions', () => [VERSION_1]), VERSION_1)
|
|
430
|
+
})
|
|
431
|
+
const ALONE = 'figure-history-panel[alone]'
|
|
432
|
+
const kept = (id, restoredAt = null) => ({ id, kind: 'revert', state: 'draft', version: 1, revertedToVersion: 1, restoredAt, createdAt: null, creator: null })
|
|
433
|
+
|
|
434
|
+
test('nothing confirmed on the server: not reverted, and said so', async () => {
|
|
435
|
+
await read(() => product.panel({ figureId: 'p', figureState: 'draft', figureVersion: 1, dirty: false, serverSource: () => undefined, saveFirst: async () => ({ ok: true }) }))
|
|
436
|
+
await press(`${ALONE} >>> li button[revert]`, 'revert v1')
|
|
437
|
+
await press('ox-prompt >>> #confirm', 'confirm the revert')
|
|
438
|
+
await until(p => product.deep(`${p} >>> p[failed]`)[0], 'the failure', ALONE)
|
|
439
|
+
assert.equal(await failure(ALONE), 'figure.text.revert-needs-server-source')
|
|
440
|
+
assert.equal((await fields()).includes('revertFigureVersion'), false)
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
test('only the newest kept draft not yet brought back is offered', async () => {
|
|
444
|
+
const offers = async drafts => {
|
|
445
|
+
await read(drafts => {
|
|
446
|
+
product.answer('figureReplacedDrafts', () => drafts)
|
|
447
|
+
return product.panel({ figureId: 'p', figureState: 'draft', figureVersion: 1, dirty: false, serverSource: () => 'x' })
|
|
448
|
+
}, drafts)
|
|
449
|
+
return until(p => { const items = product.deep(`${p} >>> section[kept] li`); return items.length === 2 && items.map(li => !!li.querySelector('button[undo]')) }, 'the kept list', ALONE)
|
|
450
|
+
}
|
|
451
|
+
assert.deepEqual(await offers([kept('k2'), kept('k1')]), [true, false])
|
|
452
|
+
assert.deepEqual(await offers([kept('k2', '2026-10-01T00:00:00Z'), kept('k1')]), [false, false])
|
|
453
|
+
})
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
/** The part's row in the parts list clicked, as the author picks it. */
|
|
457
|
+
const pick = part => read(part => [...product.deep('figure-editor >>> figure-parts >>> li')].find(li => li.textContent.includes(part)).click(), part)
|
|
458
|
+
const beforeUnload = () => read(() => { const e = new Event('beforeunload', { cancelable: true }); window.dispatchEvent(e); return e.defaultPrevented })
|
|
459
|
+
const properties = () => read(() => { const p = product.deep('figure-modeller-page >>> figure-properties')[0]; return { slot: p.assignedSlot?.name ?? null, visible: p.checkVisibility() } })
|
|
460
|
+
const hostContext = () => read(() => { const { figureId, draftSource, context, revision, selection, trying } = assistant.surface.hostContext(); return { figureId, name: draftSource.name, context, revision, selection, trying } })
|
|
461
|
+
const said = () => read(() => assistant.said())
|
|
462
|
+
async function undo() {
|
|
463
|
+
await read(() => document.activeElement?.blur())
|
|
464
|
+
await page().keyboard.down('Control')
|
|
465
|
+
await page().keyboard.press('z')
|
|
466
|
+
await page().keyboard.up('Control')
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
describe('the page around the figure: the shell, the dock, the properties', () => {
|
|
470
|
+
before(async () => {
|
|
471
|
+
await fresh()
|
|
472
|
+
await read(() => product.seed({ id: 'd', type: 'door', source: product.doorSource() }))
|
|
473
|
+
await open('d')
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
test('a saved figure: the rail marks the catalogue, and the dock is given a surface anchored on its row', async () => {
|
|
477
|
+
assert.deepEqual(await read(() => product.context()), { title: 'Door', menu: 'figure-list' })
|
|
478
|
+
assert.deepEqual((await said()).filter(s => s.call === 'provide'), [{ call: 'provide', key: 'figure:d' }])
|
|
479
|
+
const surface = await read(() => ({ subject: assistant.surface.subject, anchor: assistant.surface.anchor() }))
|
|
480
|
+
assert.deepEqual(surface, { subject: 'figure', anchor: { anchorType: 'figure', anchorId: 'd' } })
|
|
481
|
+
assert.equal(await beforeUnload(), false, 'nothing unsaved: leaving is not asked about')
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
test('the properties tab: the figure stands there while nothing is picked, and gives way to a picked part', async () => {
|
|
485
|
+
await press(TAB('properties'), 'the properties tab')
|
|
486
|
+
await until(() => product.deep('figure-modeller-page >>> figure-properties')[0]?.checkVisibility(), 'the figure properties shown')
|
|
487
|
+
assert.deepEqual(await properties(), { slot: 'figure', visible: true })
|
|
488
|
+
await pick('leaf')
|
|
489
|
+
await until(() => !product.deep('figure-modeller-page >>> figure-properties')[0].checkVisibility(), 'the part in its place')
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
test('picking parts: the dock hears the part with the version it was seen on, and is not told of the picking', async () => {
|
|
493
|
+
const before = (await said()).length
|
|
494
|
+
await pick('frame')
|
|
495
|
+
await until(() => assistant.surface.hostContext().selection?.part === 'frame', 'frame picked')
|
|
496
|
+
const { context, revision, selection } = await hostContext()
|
|
497
|
+
assert.deepEqual(selection, { part: 'frame', context, revision })
|
|
498
|
+
await pick('leaf')
|
|
499
|
+
await until(() => assistant.surface.hostContext().selection?.part === 'leaf', 'leaf picked')
|
|
500
|
+
assert.deepEqual((await said()).slice(before), [], 'neither a new conversation nor a notice')
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
test('what the preview tries is heard too, on the version it was seen on', async () => {
|
|
504
|
+
await press('figure-editor >>> button[mode]:not([aria-pressed=true])', 'the preview')
|
|
505
|
+
const slider = 'figure-editor >>> figure-preview >>> div[clips] input[type=range]'
|
|
506
|
+
await until(s => !!product.deep(s)[0], 'the clip slider', slider)
|
|
507
|
+
/* The clip's speed: its slider comes after the figure's own controls. */
|
|
508
|
+
await read(s => { const input = product.deep(s).at(-1); input.value = '1.5'; input.dispatchEvent(new Event('input')) }, slider)
|
|
509
|
+
const { context, revision, trying } = await hostContext()
|
|
510
|
+
assert.deepEqual(trying, { clips: { swing: 1.5 }, context, revision })
|
|
511
|
+
await press('figure-editor >>> button[mode]:not([aria-pressed=true])', 'the edit canvas')
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
test('renamed in the properties: one step, undone like any edit; the title follows; the dock reads the draft when asked', async () => {
|
|
515
|
+
await page().keyboard.press('Escape')
|
|
516
|
+
await until(() => product.deep('figure-modeller-page >>> figure-properties')[0]?.checkVisibility(), 'the figure properties shown')
|
|
517
|
+
const before = await hostContext()
|
|
518
|
+
await rename('Door 2')
|
|
519
|
+
await until(() => product.view().name === 'Door 2', 'renamed')
|
|
520
|
+
assert.equal((await read(() => product.view())).revision, before.revision + 1)
|
|
521
|
+
assert.equal((await read(() => product.context())).title, 'Door 2')
|
|
522
|
+
const now = await hostContext()
|
|
523
|
+
assert.deepEqual({ name: now.name, revision: now.revision }, { name: 'Door 2', revision: before.revision + 1 })
|
|
524
|
+
await undo()
|
|
525
|
+
await until(() => product.view().name === 'Door', 'undone')
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
test('fitting from the properties declares the room from the parts, as one step', async () => {
|
|
529
|
+
const before = await read(() => product.view())
|
|
530
|
+
await press('figure-properties >>> button[act]', 'fit to the parts')
|
|
531
|
+
await until(() => product.deep('figure-properties >>> div[row] > span')[0]?.textContent === 'figure.text.declared-space-declared', 'the room declared')
|
|
532
|
+
assert.equal((await read(() => product.view())).revision, before.revision + 1)
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
test('unsaved: closing or reloading the tab is asked about', async () => {
|
|
536
|
+
assert.equal((await read(() => product.view())).dirty, true)
|
|
537
|
+
assert.equal(await beforeUnload(), true)
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
test('a new figure gives the dock nothing to talk about, and the rail does not mark the catalogue', async () => {
|
|
541
|
+
await press('button[save]', 'save')
|
|
542
|
+
await until(() => !product.view().dirty, 'saved')
|
|
543
|
+
await read(() => assistant.forget())
|
|
544
|
+
await open(null)
|
|
545
|
+
assert.deepEqual(await said(), [{ call: 'clear' }], 'the saved figure\'s conversation taken back, none given')
|
|
546
|
+
assert.equal(await read(() => assistant.surface), null)
|
|
547
|
+
assert.equal((await read(() => product.context())).menu, undefined)
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
test('inactive: the surface is taken back', async () => {
|
|
551
|
+
await open('d')
|
|
552
|
+
await until(() => assistant.said().some(s => s.call === 'provide'), 'provided again')
|
|
553
|
+
await read(() => assistant.forget())
|
|
554
|
+
await read(() => product.leave())
|
|
555
|
+
assert.deepEqual(await said(), [{ call: 'clear' }])
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
test('gone: the surface is taken back, and the tab is no longer held', async () => {
|
|
559
|
+
await open('d')
|
|
560
|
+
await rename('Door 3')
|
|
561
|
+
await until(() => product.view().dirty, 'edited')
|
|
562
|
+
await read(() => assistant.forget())
|
|
563
|
+
await read(() => product.page().remove())
|
|
564
|
+
assert.deepEqual(await said(), [{ call: 'clear' }])
|
|
565
|
+
assert.equal(await beforeUnload(), false)
|
|
566
|
+
})
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
describe('AI proposals reaching the page', () => {
|
|
570
|
+
before(async () => {
|
|
571
|
+
await fresh()
|
|
572
|
+
await read(() => product.seed({ id: 'd', type: 'door', source: product.doorSource() }))
|
|
573
|
+
await open('d')
|
|
574
|
+
})
|
|
575
|
+
/** A `figure-apply` sent as the dock's handler sends it; its detail as the page left it. */
|
|
576
|
+
const offer = detail => read(detail => { window.dispatchEvent(new CustomEvent('figure-apply', { detail })); return detail }, detail)
|
|
577
|
+
|
|
578
|
+
test('the client entry registers the handler for figure proposals', async () => {
|
|
579
|
+
assert.deepEqual((await said()).filter(s => s.call === 'register'), [{ call: 'register', tool: 'proposeFigureCandidate' }])
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
test('a proposal is applied on the version the model saw, as one step; on another version it is refused', async () => {
|
|
583
|
+
const { context, revision } = await read(() => product.view())
|
|
584
|
+
const taken = await offer({ steps: LID, create: false, context, revision })
|
|
585
|
+
assert.equal(taken.applied, true)
|
|
586
|
+
const now = await read(() => product.view())
|
|
587
|
+
assert.equal(now.revision, revision + 1)
|
|
588
|
+
assert.ok(JSON.parse(now.source).appearance.some(a => a.target === 'lid'))
|
|
589
|
+
const stale = await offer({ steps: LID, create: false, context, revision })
|
|
590
|
+
assert.deepEqual({ applied: stale.applied, code: stale.code }, { applied: false, code: 'REVISION_CONFLICT' })
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
test('a proposal that would stop a clip is refused whole, the clip named', async () => {
|
|
594
|
+
const before = await read(() => product.view())
|
|
595
|
+
const refused = await offer({ steps: [{ kind: 'remove-part', target: 'leaf' }], create: false, context: before.context, revision: before.revision })
|
|
596
|
+
assert.deepEqual({ applied: refused.applied, code: refused.code, clips: refused.clips }, { applied: false, code: 'CLIPS_DROPPED', clips: ['swing'] })
|
|
597
|
+
const after = await read(() => product.view())
|
|
598
|
+
assert.deepEqual({ source: after.source, revision: after.revision }, { source: before.source, revision: before.revision })
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
test('the dock checks what became of a proposal with the server', async () => {
|
|
602
|
+
await read(() => product.answer('figureProposalOutcome', () => [{ figureId: 'd', type: 'door', name: 'Door', recordedAt: null }]))
|
|
603
|
+
const outcome = await read(key => assistant.surface.proposalOutcome(key), K1)
|
|
604
|
+
assert.deepEqual(outcome, { verdict: 'accepted', note: 'figure.text.proposal-saved-into {"names":"Door"}' })
|
|
605
|
+
assert.deepEqual(await callsTo('figureProposalOutcome'), [{ proposalKey: K1 }])
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
test("a new figure proposed: the modeller opens a new one, takes the steps under the proposal's key, and the dock is told", async () => {
|
|
609
|
+
await press('button[save]', 'save')
|
|
610
|
+
await until(() => !product.view().dirty, 'saved')
|
|
611
|
+
await read(() => product.forget())
|
|
612
|
+
const outcome = await read(
|
|
613
|
+
async ({ key, steps }) => {
|
|
614
|
+
const told = []
|
|
615
|
+
const outcome = await assistant.handlers.proposeFigureCandidate({ key, steps, create: true }, { notify: (level, note) => told.push({ level, note }) })
|
|
616
|
+
return { outcome, told }
|
|
617
|
+
},
|
|
618
|
+
{ key: K2, steps: LID }
|
|
619
|
+
)
|
|
620
|
+
assert.deepEqual(outcome, {
|
|
621
|
+
outcome: { verdict: 'accepted', applied: 1, note: 'figure.text.proposal-applied {"n":"1"}' },
|
|
622
|
+
told: [{ level: 'info', note: 'figure.text.proposal-applied {"n":"1"}' }]
|
|
623
|
+
})
|
|
624
|
+
assert.deepEqual((await read(() => product.navigations())).map(n => n.path), ['/figure-modeller'])
|
|
625
|
+
const made = await read(() => product.view())
|
|
626
|
+
assert.equal(made.id, undefined)
|
|
627
|
+
assert.ok(JSON.parse(made.source).appearance.some(a => a.target === 'lid'))
|
|
628
|
+
await press('button[save]', 'save')
|
|
629
|
+
await press('figure-first-save >>> md-filled-button[slot=confirm]', 'the first save')
|
|
630
|
+
await until(() => product.view().id, 'saved')
|
|
631
|
+
const [saved] = await callsTo('saveFigure')
|
|
632
|
+
assert.deepEqual({ creationId: saved.creationId, proposalKeys: saved.proposalKeys }, { creationId: K2, proposalKeys: [K2] })
|
|
633
|
+
})
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
describe('the catalogue preview', () => {
|
|
637
|
+
before(async () => {
|
|
638
|
+
await fresh()
|
|
639
|
+
await read(() => product.seed({ id: 'a', type: 'agv', source: product.boxSource('A') }))
|
|
640
|
+
})
|
|
641
|
+
const shown = () => read(() => product.detailShown())
|
|
642
|
+
|
|
643
|
+
test('it belongs to the catalogue: another page closes it, moving within the catalogue does not', async () => {
|
|
644
|
+
await read(() => {
|
|
645
|
+
product.goTo('/figure-list')
|
|
646
|
+
product.openDetail('a')
|
|
647
|
+
})
|
|
648
|
+
await until(() => product.deep('figure-detail-dock')[0]?.isConnected, 'the preview open')
|
|
649
|
+
await read(() => product.goTo('/figure-list/a'))
|
|
650
|
+
assert.equal(await shown(), true)
|
|
651
|
+
await read(() => product.goTo('/board-list'))
|
|
652
|
+
assert.equal(await shown(), false)
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
test('editing from it closes it before the modeller opens', async () => {
|
|
656
|
+
await read(() => {
|
|
657
|
+
product.goTo('/figure-list')
|
|
658
|
+
product.forget()
|
|
659
|
+
product.openDetail('a')
|
|
660
|
+
})
|
|
661
|
+
await press('figure-detail-dock >>> footer button', 'edit')
|
|
662
|
+
await until(() => product.navigations().length, 'the modeller opened')
|
|
663
|
+
assert.deepEqual(await read(() => product.navigations().map(({ path, detailShown }) => ({ path, detailShown }))), [{ path: '/figure-modeller/a', detailShown: false }])
|
|
664
|
+
})
|
|
665
|
+
})
|
|
666
|
+
|
|
667
|
+
describe('the page at a narrow width', () => {
|
|
668
|
+
before(async () => {
|
|
669
|
+
await fresh()
|
|
670
|
+
await page().setViewport({ width: 945, height: 900 })
|
|
671
|
+
await read(() => product.seed({ id: 'a', type: 'agv', source: product.boxSource('A') }))
|
|
672
|
+
await open('a')
|
|
673
|
+
})
|
|
674
|
+
after(() => page().setViewport({ width: 1400, height: 900 }))
|
|
675
|
+
|
|
676
|
+
test('the page fits the width it is given: the history panel does not run off its right edge', async () => {
|
|
677
|
+
await press(TAB('history'), 'the history tab')
|
|
678
|
+
await until(p => product.deep(p)[0]?.checkVisibility(), 'the history panel', PANEL)
|
|
679
|
+
const edges = await read(p => ({ panel: product.deep(p)[0].getBoundingClientRect().right, page: product.page().getBoundingClientRect().right, window: innerWidth }), PANEL)
|
|
680
|
+
assert.ok(edges.panel <= edges.page && edges.page <= edges.window, JSON.stringify(edges))
|
|
681
|
+
})
|
|
682
|
+
})
|