@things-factory/figure-ui 10.1.70 โ 10.1.72
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/graphql/index.ts +28 -69
- package/client/modeller/figure-v3-parts.ts +16 -136
- package/client/modeller/figure-v3-recipe-catalog.ts +366 -0
- package/client/modeller/v3-editor.ts +25 -5
- package/client/modeller/v3-fields.ts +21 -16
- package/client/modeller/v3-gizmo.ts +1 -1
- package/client/modeller/v3-links.ts +7 -66
- package/client/modeller/v3-palette.ts +3 -0
- package/client/modeller/v3-recipe-source.ts +15 -0
- package/client/modeller/v3-recipes.ts +50 -27
- package/client/pages/figure-v3-modeller-page.ts +16 -13
- package/client/route.ts +0 -4
- package/dist-client/graphql/index.d.ts +11 -18
- package/dist-client/graphql/index.js +18 -59
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-v3-parts.d.ts +5 -12
- package/dist-client/modeller/figure-v3-parts.js +17 -147
- package/dist-client/modeller/figure-v3-parts.js.map +1 -1
- package/dist-client/modeller/figure-v3-recipe-catalog.d.ts +38 -0
- package/dist-client/modeller/figure-v3-recipe-catalog.js +399 -0
- package/dist-client/modeller/figure-v3-recipe-catalog.js.map +1 -0
- package/dist-client/modeller/v3-clips.d.ts +1 -1
- package/dist-client/modeller/v3-editor.d.ts +8 -3
- package/dist-client/modeller/v3-editor.js +35 -6
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-fields.d.ts +2 -2
- package/dist-client/modeller/v3-fields.js +20 -18
- package/dist-client/modeller/v3-fields.js.map +1 -1
- package/dist-client/modeller/v3-gizmo.js +1 -1
- package/dist-client/modeller/v3-gizmo.js.map +1 -1
- package/dist-client/modeller/v3-links.d.ts +8 -25
- package/dist-client/modeller/v3-links.js +4 -66
- package/dist-client/modeller/v3-links.js.map +1 -1
- package/dist-client/modeller/v3-palette.js +3 -0
- package/dist-client/modeller/v3-palette.js.map +1 -1
- package/dist-client/modeller/v3-recipe-source.d.ts +8 -0
- package/dist-client/modeller/v3-recipe-source.js +14 -0
- package/dist-client/modeller/v3-recipe-source.js.map +1 -0
- package/dist-client/modeller/v3-recipes.d.ts +25 -14
- package/dist-client/modeller/v3-recipes.js +42 -28
- package/dist-client/modeller/v3-recipes.js.map +1 -1
- package/dist-client/pages/figure-v3-modeller-page.d.ts +5 -3
- package/dist-client/pages/figure-v3-modeller-page.js +16 -13
- package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +0 -3
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.fui.tsbuildinfo +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -4
- package/scripts/template-thumbnails.mjs +53 -0
- package/test/browser/build.mjs +15 -3
- package/test/browser/modeller-page.js +26 -3
- package/test/browser/modeller.check.mjs +73 -46
- package/test/browser/serve.mjs +7 -1
- package/test/browser/thumbnail-page.js +14 -0
- package/test/v3-fields.test.ts +5 -1
- package/test/v3-links.test.ts +12 -0
- package/test/v3-recipes.test.ts +47 -25
- package/things-factory.config.js +0 -1
- package/translations/en.json +38 -23
- package/translations/ja.json +38 -23
- package/translations/ko.json +38 -23
- package/translations/ms.json +38 -23
- package/translations/zh.json +38 -23
- package/client/pages/figure-recipe-page.ts +0 -338
- package/dist-client/pages/figure-recipe-page.d.ts +0 -41
- package/dist-client/pages/figure-recipe-page.js +0 -352
- package/dist-client/pages/figure-recipe-page.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/figure-ui",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.72",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -27,21 +27,22 @@
|
|
|
27
27
|
"clean": "npm run clean:server && npm run clean:client"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@hatiolab/figure-model": "^0.1.
|
|
30
|
+
"@hatiolab/figure-model": "^0.1.87",
|
|
31
31
|
"@hatiolab/things-scene": "^10.1.56",
|
|
32
32
|
"@material/web": "^2.0.0",
|
|
33
33
|
"@operato/data-grist": "^10.0.0",
|
|
34
34
|
"@operato/graphql": "^10.0.0",
|
|
35
35
|
"@operato/i18n": "^10.0.0",
|
|
36
36
|
"@operato/layout": "^10.28.0",
|
|
37
|
+
"@operato/markdown": "^10.23.0",
|
|
37
38
|
"@operato/p13n": "^10.0.0",
|
|
38
39
|
"@operato/popup": "^10.23.0",
|
|
39
40
|
"@operato/shell": "^10.0.0",
|
|
40
41
|
"@operato/styles": "^10.0.0",
|
|
41
42
|
"@operato/utils": "^10.0.0",
|
|
42
43
|
"@things-factory/ai-assistant": "^10.1.58",
|
|
43
|
-
"@things-factory/figure-service": "^10.1.
|
|
44
|
+
"@things-factory/figure-service": "^10.1.72",
|
|
44
45
|
"three": "^0.185.1"
|
|
45
46
|
},
|
|
46
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "fdf6d4faac454dbdca62d8fe7409395dff1ada22"
|
|
47
48
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Draws each recipe template's thumbnail from its model (ADR-0095 amended: model first, verified, then the recipe).
|
|
3
|
+
*
|
|
4
|
+
* node scripts/template-thumbnails.mjs [name โฆ]
|
|
5
|
+
* FIGURE_MODEL=../../../figure-model node scripts/template-thumbnails.mjs (a figure-model not published yet)
|
|
6
|
+
*
|
|
7
|
+
* Reads figure-service/recipes/<name>/models/default.json (the model at the template's defaults) and writes
|
|
8
|
+
* figure-service/recipes/<name>/thumbnail.png, drawn open when the model has a control, so the card shows the motion.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
11
|
+
import { dirname, join, resolve } from 'node:path'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
|
|
14
|
+
import { buildPage } from '../test/browser/build.mjs'
|
|
15
|
+
import { openPage } from '../test/browser/serve.mjs'
|
|
16
|
+
|
|
17
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
18
|
+
const RECIPES = resolve(HERE, '../../figure-service/recipes')
|
|
19
|
+
const only = process.argv.slice(2)
|
|
20
|
+
|
|
21
|
+
const opened = await openPage(await buildPage('thumbnail-page.js'))
|
|
22
|
+
try {
|
|
23
|
+
for (const name of readdirSync(RECIPES).filter(n => !only.length || only.includes(n)).sort()) {
|
|
24
|
+
const model = join(RECIPES, name, 'models', 'default.json')
|
|
25
|
+
if (!existsSync(model)) continue
|
|
26
|
+
const text = readFileSync(model, 'utf-8')
|
|
27
|
+
const asset = JSON.parse(text)
|
|
28
|
+
/*
|
|
29
|
+
Each control most of the way from where it rests toward its far end: a door partly open (a left hinge opens toward
|
|
30
|
+
-90), so the card shows what moves -- a sliding leaf opened all the way hides behind its jamb.
|
|
31
|
+
*/
|
|
32
|
+
const rest = asset.stateDefaults ?? {}
|
|
33
|
+
const state = Object.fromEntries(
|
|
34
|
+
asset.document.model.inputs
|
|
35
|
+
.filter(i => i.role === 'state')
|
|
36
|
+
.map(i => {
|
|
37
|
+
const from = rest[i.id] ?? 0
|
|
38
|
+
const far = Math.abs(i.max - from) >= Math.abs(i.min - from) ? i.max : i.min
|
|
39
|
+
return [i.id, from + (far - from) * 0.6]
|
|
40
|
+
})
|
|
41
|
+
)
|
|
42
|
+
const url = await opened.page.evaluate((text, state) => window.shoot(text, state), text, state)
|
|
43
|
+
if (!url) {
|
|
44
|
+
console.error(`${name}: not drawn`)
|
|
45
|
+
process.exitCode = 1
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
writeFileSync(join(RECIPES, name, 'thumbnail.png'), Buffer.from(url.split(',')[1], 'base64'))
|
|
49
|
+
console.log(`${name}: thumbnail.png`)
|
|
50
|
+
}
|
|
51
|
+
} finally {
|
|
52
|
+
await opened.close()
|
|
53
|
+
}
|
package/test/browser/build.mjs
CHANGED
|
@@ -19,8 +19,8 @@ const HERE = dirname(fileURLToPath(import.meta.url))
|
|
|
19
19
|
const PACKAGE = resolve(HERE, '../..')
|
|
20
20
|
const ROOT = resolve(PACKAGE, '../..')
|
|
21
21
|
|
|
22
|
-
export async function buildPage() {
|
|
23
|
-
const out = join(tmpdir(), 'figure-ui-browser-
|
|
22
|
+
export async function buildPage(page = 'modeller-page.js') {
|
|
23
|
+
const out = join(tmpdir(), 'figure-ui-browser-' + page.replace(/\W+/g, '-'))
|
|
24
24
|
const emitted = join(out, 'client')
|
|
25
25
|
rmSync(out, { recursive: true, force: true })
|
|
26
26
|
mkdirSync(out, { recursive: true })
|
|
@@ -34,7 +34,7 @@ export async function buildPage() {
|
|
|
34
34
|
if (figureModel && !existsSync(figureModel)) throw Error(`FIGURE_MODEL has no build: ${figureModel}`)
|
|
35
35
|
|
|
36
36
|
const bundle = await rollup({
|
|
37
|
-
input: join(HERE,
|
|
37
|
+
input: join(HERE, page),
|
|
38
38
|
onwarn(warning, warn) {
|
|
39
39
|
if (warning.code === 'CIRCULAR_DEPENDENCY' || warning.code === 'THIS_IS_UNDEFINED' || warning.code === 'EVAL') return
|
|
40
40
|
warn(warning)
|
|
@@ -56,6 +56,18 @@ export async function buildPage() {
|
|
|
56
56
|
const file = join(out, 'page.js')
|
|
57
57
|
await bundle.write({ file, format: 'esm', inlineDynamicImports: true })
|
|
58
58
|
await bundle.close()
|
|
59
|
+
|
|
60
|
+
/*
|
|
61
|
+
ox-markdown renders in a worker it loads from beside its module (\`../../workers/markdown-webworker.js\`); from
|
|
62
|
+
/page.js that is /workers/. Bundled here and served there, the guides render as in the app -- without it the worker
|
|
63
|
+
never answers and a guide is drawn empty.
|
|
64
|
+
*/
|
|
65
|
+
const worker = await rollup({
|
|
66
|
+
input: join(ROOT, 'node_modules/@operato/markdown/workers/markdown-webworker.js'),
|
|
67
|
+
plugins: [nodeResolve({ browser: true, modulePaths: [join(PACKAGE, 'node_modules'), join(ROOT, 'node_modules')] }), commonjs()]
|
|
68
|
+
})
|
|
69
|
+
await worker.write({ file: join(out, 'workers/markdown-webworker.js'), format: 'esm', inlineDynamicImports: true })
|
|
70
|
+
await worker.close()
|
|
59
71
|
return file
|
|
60
72
|
}
|
|
61
73
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import 'figure-ui-client/modeller/v3-editor.js'
|
|
7
7
|
import { createV3ProductSession } from 'figure-ui-client/modeller/v3-product-session.js'
|
|
8
|
-
import { addV3Box, createV3Asset, robotArmV3, serializeV3Asset } from '@hatiolab/figure-model'
|
|
8
|
+
import { addV3Box, addV3Primitive, applyV3Authoring, createV3Asset, robotArmV3, serializeV3Asset } from '@hatiolab/figure-model'
|
|
9
9
|
|
|
10
10
|
const PALETTE = {
|
|
11
11
|
'palette.dark': '#2b2b2b',
|
|
@@ -36,6 +36,20 @@ function sourceOf(kind, text) {
|
|
|
36
36
|
asset.palette = { ...asset.palette, ...PALETTE }
|
|
37
37
|
return serializeV3Asset(addV3Box(asset, { id: 'room', size: { x: 4000, y: 3000, z: 6000 }, position: { x: 0, y: 1500, z: 0 } }))
|
|
38
38
|
}
|
|
39
|
+
if (kind === 'rollers') {
|
|
40
|
+
/* A roller row whose rollers spin in place, each carrying a mark (ADR-0093, 2026-09-26). */
|
|
41
|
+
let asset = createV3Asset('Rollers')
|
|
42
|
+
asset.palette = { ...asset.palette, ...PALETTE }
|
|
43
|
+
asset = addV3Box(asset, { id: 'bed', size: { x: 2000, y: 400, z: 600 }, position: { x: 0, y: 200, z: 0 } })
|
|
44
|
+
asset = applyV3Authoring(asset, { kind: 'attach', part: 'bed', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
45
|
+
asset = addV3Primitive(asset, { id: 'roller', kind: 'cylinder', dimensions: { radius: 40, length: 560 }, position: { x: 0, y: 0, z: 0 }, rotation: { x: 90, y: 0, z: 0 } })
|
|
46
|
+
asset = applyV3Authoring(asset, { kind: 'attach', part: 'roller', face: 'centre', to: { part: 'bed', face: 'top' }, gap: 40 })
|
|
47
|
+
asset = addV3Box(asset, { id: 'mark', size: { x: 30, y: 30, z: 560 }, position: { x: 0, y: 0, z: 0 } })
|
|
48
|
+
asset = applyV3Authoring(asset, { kind: 'attach', part: 'mark', face: 'left', to: { part: 'roller', face: 'right' } })
|
|
49
|
+
const j = asset.joints.find(o => o.body1.part === 'roller')
|
|
50
|
+
asset = applyV3Authoring(asset, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Z', lowerLimit: -360, upperLimit: 360, state: { id: 'spin' } } })
|
|
51
|
+
return serializeV3Asset(applyV3Authoring(asset, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 }))
|
|
52
|
+
}
|
|
39
53
|
throw Error(`no figure for ${kind}`)
|
|
40
54
|
}
|
|
41
55
|
|
|
@@ -139,6 +153,14 @@ window.probe = {
|
|
|
139
153
|
const l = Math.hypot(b.x - a.x, b.y - a.y) || 1
|
|
140
154
|
return { x: (b.x - a.x) / l, y: (b.y - a.y) / l }
|
|
141
155
|
},
|
|
156
|
+
/** The ids of every body drawn. */
|
|
157
|
+
ids() {
|
|
158
|
+
return editor().session.read().geometry.map(g => g.id)
|
|
159
|
+
},
|
|
160
|
+
/** Whether an input with this label is on the page. */
|
|
161
|
+
hasInput(label) {
|
|
162
|
+
return deep('input').some(i => i.getAttribute('aria-label') === label)
|
|
163
|
+
},
|
|
142
164
|
/** The page point of a part's row in the parts list. */
|
|
143
165
|
row(id) {
|
|
144
166
|
const label = deep('span[label]').find(s => s.getAttribute('title') === id)
|
|
@@ -191,7 +213,7 @@ window.probe = {
|
|
|
191
213
|
}
|
|
192
214
|
|
|
193
215
|
window.harness = {
|
|
194
|
-
async mount(kind, text,
|
|
216
|
+
async mount(kind, text, templates = [], guides = {}) {
|
|
195
217
|
const source = sourceOf(kind, text)
|
|
196
218
|
const session = createV3ProductSession({
|
|
197
219
|
load: async id => ({ id, source }),
|
|
@@ -200,7 +222,8 @@ window.harness = {
|
|
|
200
222
|
await session.open('probe')
|
|
201
223
|
const editor = document.createElement('figure-v3-editor')
|
|
202
224
|
editor.session = session
|
|
203
|
-
editor.
|
|
225
|
+
editor.templates = templates
|
|
226
|
+
editor.guideOf = async name => guides[name] ?? ''
|
|
204
227
|
editor.revision = 0
|
|
205
228
|
/* The modeller page counts a change the same way: the editor re-reads the session on a new revision. */
|
|
206
229
|
editor.addEventListener('v3-changed', () => (editor.revision = editor.revision + 1))
|
|
@@ -13,9 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { test, describe, before, after } from 'node:test'
|
|
15
15
|
import assert from 'node:assert/strict'
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { dirname, join, resolve } from 'node:path'
|
|
16
|
+
import { readFileSync } from 'node:fs'
|
|
17
|
+
import { dirname, resolve } from 'node:path'
|
|
19
18
|
import { fileURLToPath } from 'node:url'
|
|
20
19
|
|
|
21
20
|
import { buildPage } from './build.mjs'
|
|
@@ -674,13 +673,13 @@ describe('a part follows the figure size in one step (follow-size, 2026-09-26)',
|
|
|
674
673
|
})
|
|
675
674
|
})
|
|
676
675
|
|
|
677
|
-
describe('
|
|
678
|
-
/* The
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
676
|
+
describe('the recipe catalog: a template chosen, filled in, put onto the chosen part in one step (ADR-0095)', () => {
|
|
677
|
+
/* The product's templates, as the server reads them, as catalog rows. */
|
|
678
|
+
const templates = () =>
|
|
679
|
+
['sliding-door', 'hinged-door', 'roller-row'].map(name => {
|
|
680
|
+
const recipe = JSON.parse(readFileSync(resolve(HERE, `../../../figure-service/recipes/${name}/recipe.json`), 'utf-8'))
|
|
681
|
+
return { name, category: recipe.category, tags: recipe.tags, target: recipe.target, title: recipe.title.en, description: recipe.description.en, hasThumbnail: false, recipe }
|
|
682
|
+
})
|
|
684
683
|
const at = selector =>
|
|
685
684
|
page().evaluate(selector => {
|
|
686
685
|
const deep = (root = document) => [...root.querySelectorAll(selector)].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
@@ -688,58 +687,86 @@ describe('a recipe put onto a part: chosen, filled in, one step (ADR-0095, 2026-
|
|
|
688
687
|
if (!e) return null
|
|
689
688
|
e.scrollIntoView({ block: 'center' })
|
|
690
689
|
const r = e.getBoundingClientRect()
|
|
691
|
-
return { x: r.left + r.width / 2, y: r.top + r.height / 2, disabled: !!e.disabled }
|
|
690
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2, disabled: !!e.disabled, text: e.textContent.trim() }
|
|
692
691
|
}, selector)
|
|
693
692
|
const parts = () => read(() => document.querySelector('figure-v3-editor').session.read().geometry.map(g => g.id))
|
|
693
|
+
const type = async (selector, text) => {
|
|
694
|
+
const field = await page().evaluateHandle(selector => {
|
|
695
|
+
const deep = (root = document) => [...root.querySelectorAll(selector)].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
696
|
+
return deep()[0] ?? null
|
|
697
|
+
}, selector)
|
|
698
|
+
await field.evaluate(i => i.scrollIntoView({ block: 'center' }))
|
|
699
|
+
await field.click({ clickCount: 3 })
|
|
700
|
+
await page().keyboard.type(text)
|
|
701
|
+
await page().keyboard.press('Tab')
|
|
702
|
+
}
|
|
694
703
|
|
|
695
|
-
test('
|
|
696
|
-
|
|
704
|
+
test('the room chosen, the catalog opened: the door told in full, a share typed, put in -- the frame, the leaves and the lamp, and the catalog closes', async () => {
|
|
705
|
+
const guides = { 'sliding-door': readFileSync(resolve(HERE, '../../../figure-service/recipes/sliding-door/guide.en.md'), 'utf-8') }
|
|
706
|
+
await read((list, guides) => harness.mount('room', null, list, guides), templates(), guides)
|
|
697
707
|
await sleep(2500)
|
|
698
|
-
const idle = await at('button[recipe="sliding-door"]')
|
|
699
|
-
assert.ok(idle, 'the sliding door is offered')
|
|
700
|
-
assert.ok(idle.disabled, 'it waits for a part to be chosen')
|
|
701
708
|
await click(await read(() => probe.row('room')))
|
|
702
|
-
await click(await at('button[recipe
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
709
|
+
await click(await at('button[recipe-catalog]'))
|
|
710
|
+
assert.ok(await at('figure-v3-recipe-catalog'), 'the catalog is open')
|
|
711
|
+
await click(await at('button[card="hinged-door"]'))
|
|
712
|
+
/* A search that takes the chosen card away tells the first card it leaves (:3300, 2026-09-26). */
|
|
713
|
+
await type('input[search]', 'sliding')
|
|
714
|
+
assert.equal(await read(() => {
|
|
715
|
+
const deep = (root = document) => [...root.querySelectorAll('button[card][on]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
716
|
+
return deep().map(b => b.getAttribute('card')).join()
|
|
717
|
+
}), 'sliding-door', 'the card told is one the search shows')
|
|
718
|
+
assert.ok(await at('li[parts="2โ4"]'), 'the parts it adds, worked out from its choices: 2 to 4')
|
|
719
|
+
/* The guide is drawn as its text, not as the page its words would name (seen on :3300, 2026-09-26). */
|
|
720
|
+
await sleep(1500)
|
|
721
|
+
const guide = await read(() => {
|
|
722
|
+
const deep = (root = document) => [...root.querySelectorAll('ox-markdown')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
723
|
+
const shown = deep()[0]
|
|
724
|
+
return shown ? { headings: [...shown.shadowRoot.querySelectorAll('h2')].map(h => h.textContent.trim()), app: !!shown.shadowRoot.querySelector('ox-app, things-app, body, html') } : null
|
|
706
725
|
})
|
|
707
|
-
assert.
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
await
|
|
711
|
-
await page().keyboard.press('Tab')
|
|
726
|
+
assert.ok(guide, 'the guide is shown')
|
|
727
|
+
assert.ok(guide.headings.includes('When to use it'), `the guide's own headings: ${JSON.stringify(guide.headings)}`)
|
|
728
|
+
assert.equal(guide.app, false, 'no page drawn inside it')
|
|
729
|
+
await type('input[param="share"]', '50')
|
|
712
730
|
const start = await state()
|
|
713
|
-
await click(await at('
|
|
714
|
-
|
|
715
|
-
assert.equal(
|
|
716
|
-
assert.equal(await read(() => document.querySelector('figure-v3-editor').recipeError), '', 'the recipe went in')
|
|
717
|
-
assert.equal(after.revision, start.revision + 1, 'one step')
|
|
731
|
+
await click(await at('button[insert]'))
|
|
732
|
+
assert.equal(await read(() => document.querySelector('figure-v3-editor').recipeError), '', 'not refused')
|
|
733
|
+
assert.equal((await state()).revision, start.revision + 1, 'one step')
|
|
718
734
|
const ids = await parts()
|
|
719
735
|
for (const part of ['frame', 'left', 'right', 'lamp']) assert.ok(ids.includes(`sliding-door-1-${part}`), `${part} is in`)
|
|
720
736
|
const frame = await body('sliding-door-1-frame')
|
|
721
|
-
assert.equal(Math.round(frame.dimensions.right - frame.dimensions.left), 2000,
|
|
737
|
+
assert.equal(Math.round(frame.dimensions.right - frame.dimensions.left), 2000, "half the room's 4000 mm")
|
|
738
|
+
assert.equal(await at('figure-v3-recipe-catalog'), null, 'the catalog closes')
|
|
722
739
|
})
|
|
723
740
|
|
|
724
|
-
test('a door the room cannot take is refused in the
|
|
741
|
+
test('a door the room cannot take is refused in the catalog, in the kernel\'s words, and nothing is added', async () => {
|
|
742
|
+
await click(await read(() => probe.row('room')))
|
|
743
|
+
await click(await at('button[recipe-catalog]'))
|
|
744
|
+
await click(await at('button[card="sliding-door"]'))
|
|
725
745
|
const before = await parts()
|
|
726
|
-
|
|
727
|
-
const deep = (root = document) => [...root.querySelectorAll('input[param="share"]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
728
|
-
return deep()[0] ?? null
|
|
729
|
-
})
|
|
730
|
-
await share.evaluate(i => i.scrollIntoView({ block: 'center' }))
|
|
731
|
-
await share.click({ clickCount: 3 })
|
|
732
|
-
await page().keyboard.type('5')
|
|
733
|
-
await page().keyboard.press('Tab')
|
|
746
|
+
await type('input[param="share"]', '5')
|
|
734
747
|
const start = await state()
|
|
735
|
-
await click(await at('
|
|
748
|
+
await click(await at('button[insert]'))
|
|
736
749
|
assert.equal((await state()).revision, start.revision, 'no step')
|
|
737
750
|
assert.deepEqual(await parts(), before, 'nothing added')
|
|
738
|
-
const said = await
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
751
|
+
const said = await at('section[detail] p[role="alert"]')
|
|
752
|
+
assert.ok(said && /steps\[\d+\]/.test(said.text), `the kernel's sentence, with the step: ${said?.text}`)
|
|
753
|
+
await click(await at('button[close]'))
|
|
754
|
+
})
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
describe('a row that carries a part (ADR-0093, 2026-09-26)', () => {
|
|
758
|
+
test('the list has the roller and its mark, each once; a click on a mark on the canvas picks the mark, and only the roller offers the row', async () => {
|
|
759
|
+
await mount('rollers')
|
|
760
|
+
assert.ok(await read(() => probe.row('roller')), 'the roller, once')
|
|
761
|
+
assert.ok(await read(() => probe.row('mark')), 'the mark it carries, once')
|
|
762
|
+
const copy = (await read(() => probe.ids())).find(id => id.includes('mark.') && id.includes(',3,'))
|
|
763
|
+
assert.ok(copy, 'a mark on the fourth roller is drawn')
|
|
764
|
+
await click(await read(id => probe.partOnPage(id), copy))
|
|
765
|
+
assert.equal((await state()).selected, 'mark', 'picked by the part it copies')
|
|
766
|
+
assert.equal(await read(() => probe.hasInput('mark.repeat.pitch')), false, 'the mark has no row of its own to set')
|
|
767
|
+
await click(await read(() => probe.row('roller')))
|
|
768
|
+
assert.equal((await state()).selected, 'roller')
|
|
769
|
+
assert.ok(await read(() => probe.hasInput('roller.repeat.pitch')), 'the roller offers its row')
|
|
743
770
|
})
|
|
744
771
|
})
|
|
745
772
|
|
package/test/browser/serve.mjs
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Opens the built page in a headless browser: a tiny server for page.js, and puppeteer on it at a desktop size.
|
|
3
3
|
*/
|
|
4
4
|
import { createServer } from 'node:http'
|
|
5
|
-
import { readFileSync } from 'node:fs'
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
6
|
+
import { dirname, join, normalize } from 'node:path'
|
|
6
7
|
import puppeteer from 'puppeteer'
|
|
7
8
|
|
|
8
9
|
const HTML = `<!DOCTYPE html>
|
|
@@ -16,9 +17,14 @@ const HTML = `<!DOCTYPE html>
|
|
|
16
17
|
export async function openPage(bundle, { width = 1400, height = 900 } = {}) {
|
|
17
18
|
const js = readFileSync(bundle)
|
|
18
19
|
const server = createServer((req, res) => {
|
|
20
|
+
/* Scripts built beside the page (the markdown worker) are served as they are. */
|
|
21
|
+
const beside = join(dirname(bundle), normalize(req.url.split('?')[0]).replace(/^([/\\])+/, ''))
|
|
19
22
|
if (req.url === '/page.js') {
|
|
20
23
|
res.writeHead(200, { 'Content-Type': 'application/javascript' })
|
|
21
24
|
res.end(js)
|
|
25
|
+
} else if (req.url.endsWith('.js') && existsSync(beside)) {
|
|
26
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript' })
|
|
27
|
+
res.end(readFileSync(beside))
|
|
22
28
|
} else {
|
|
23
29
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
24
30
|
res.end(HTML)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The page that draws recipe template thumbnails (ADR-0095 amended): a model -- the host and the template's parts -- drawn
|
|
3
|
+
* the way the modeller draws a figure's card, by the same code (`captureV3Thumbnail`).
|
|
4
|
+
*/
|
|
5
|
+
import { captureV3Thumbnail } from 'figure-ui-client/modeller/v3-thumbnail.js'
|
|
6
|
+
import { drawableV3Parts } from 'figure-ui-client/modeller/v3-renderer.js'
|
|
7
|
+
import { compileV3Asset } from '@hatiolab/figure-model'
|
|
8
|
+
|
|
9
|
+
window.harness = { ready: true }
|
|
10
|
+
window.shoot = async (text, state = {}) => {
|
|
11
|
+
const asset = JSON.parse(text)
|
|
12
|
+
const evaluation = compileV3Asset(asset).evaluate({ ...asset.stateDefaults, ...state })
|
|
13
|
+
return await captureV3Thumbnail(drawableV3Parts(evaluation.geometry), evaluation.palette ?? asset.palette ?? {})
|
|
14
|
+
}
|
package/test/v3-fields.test.ts
CHANGED
|
@@ -53,7 +53,7 @@ test('๊ธด ์ค๋ช
์ ์ ๋ชฉ ์ ใ?ใ ๋ก ์ฐ๋ค โ ๋ ํผ์ณ ๋์ง ์๋
|
|
|
53
53
|
|
|
54
54
|
test('๋ํ์ดํ ๋ถํ์ ๋ํ์ด์ ์ด๋ฆ๊ณผ ๋ช ๋ฒ์งธ์ธ์ง๋ก ์ฝ๋๋ค โ ๋ id ๋ฅผ ๋ณด์ด์ง ์๋๋ค', () => {
|
|
55
55
|
assert.equal(partLabelOf('["roller.repeat",0,"roller.value"]'), 'roller 1')
|
|
56
|
-
assert.equal(partLabelOf('["
|
|
56
|
+
assert.equal(partLabelOf('["roller.repeat",2,"mark.placed"]'), 'mark 3', '์ค์ ๋ถํ์ ๋ถ์ ๊ฒ์ ์ ์ด๋ฆ์ผ๋ก (ADR-0093, 2026-09-26)')
|
|
57
57
|
assert.equal(partLabelOf('deck'), 'deck', '๋ณดํต ๋ถํ์ ์ ์ด๋ฆ')
|
|
58
58
|
assert.equal(partLabelOf('[not json'), '[not json')
|
|
59
59
|
const parts = readFileSync(fileURLToPath(new URL('../client/modeller/figure-v3-parts.ts', import.meta.url)), 'utf8')
|
|
@@ -64,6 +64,9 @@ test('๋ํ์ดํ ๋ถํ์ ์์ฑ ์ด๋ฆ ์นธ๋ ์ฌ๋์ ์ด๋ฆ์ด๊ณ , ๋ช
|
|
|
64
64
|
const geometry = Array.from({ length: 16 }, (_, i) => ({ id: JSON.stringify(['roller.repeat', i, 'roller.value']) })).concat([{ id: 'deck' }])
|
|
65
65
|
assert.deepEqual(repeatOf('["roller.repeat",14,"roller.value"]', geometry), { name: 'roller', count: 16, index: 15 })
|
|
66
66
|
assert.equal(repeatOf('deck', geometry), null)
|
|
67
|
+
const carried = geometry.concat(Array.from({ length: 16 }, (_, i) => ({ id: JSON.stringify(['roller.repeat', i, 'mark.placed']) })))
|
|
68
|
+
assert.deepEqual(repeatOf('["roller.repeat",14,"roller.value"]', carried), { name: 'roller', count: 16, index: 15 }, '๋กค๋ฌ์ ๋ถ์ ๊ฒ์ ๋กค๋ฌ ์์ ์ ๋ ๋ค')
|
|
69
|
+
assert.deepEqual(repeatOf('["roller.repeat",2,"mark.placed"]', carried), { name: 'mark', count: 16, index: 3 })
|
|
67
70
|
const editor = readFileSync(fileURLToPath(new URL('../client/modeller/v3-editor.ts', import.meta.url)), 'utf8')
|
|
68
71
|
assert.ok(editor.includes('.value=${partLabelOf(this.selected)}'), '์ฌ์ฉ์ 2026-09-24: ใ๋ถํ ์ด๋ฆ ???ใ')
|
|
69
72
|
assert.ok(editor.includes('?disabled=${unavailable || !!repeatOf(this.selected, view.geometry)}'), '๋ณต์ฌ๋ณธ์๋ ๋ฐ๊ฟ ์ ์ด๋ฆ์ด ์๋ค')
|
|
@@ -72,6 +75,7 @@ test('๋ํ์ดํ ๋ถํ์ ์์ฑ ์ด๋ฆ ์นธ๋ ์ฌ๋์ ์ด๋ฆ์ด๊ณ , ๋ช
|
|
|
72
75
|
test('ํธ์ง๊ธฐ๋ ์ ์ํ ๋ถํ ๋จ์๋ก ๊ณ ๋ฅด๊ณ ๋ณด์ธ๋ค โ ๋ํ์ด๋ ํ ๋ถํ, ์บ๋ฒ์ค์์๋ ๋ณต์ฌ๋ณธ ๋ชจ๋๊ฐ ๊ณจ๋ผ์ง๋ค', () => {
|
|
73
76
|
assert.equal(ownerOf('["roller.repeat",14,"roller.value"]'), 'roller')
|
|
74
77
|
assert.equal(ownerOf('deck'), 'deck')
|
|
78
|
+
assert.equal(ownerOf('["roller.repeat",2,"mark.placed"]'), 'mark', '๋กค๋ฌ์ ๋ถ์ ํ์์ ๋ณต์ฌ๋ณธ์ ๋๋ฅด๋ฉด ๊ทธ ํ์๋ฅผ ๊ณ ๋ฅธ๋ค')
|
|
75
79
|
assert.deepEqual(repeatOf('roller', [{ id: '["roller.repeat",0,"roller.value"]' }, { id: '["roller.repeat",1,"roller.value"]' }, { id: 'deck' }]), { name: 'roller', count: 2, index: 0 })
|
|
76
80
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
77
81
|
const canvas = read('../client/modeller/v3-canvas.ts')
|
package/test/v3-links.test.ts
CHANGED
|
@@ -618,3 +618,15 @@ test('์น์ ์นธ โ ใ๋ํ ํฌ๊ธฐ๋ฅผ ๋ฐ๋ผ๊ฐใ ๋จ์ถ๋ follow-size ํ
|
|
|
618
618
|
const ko = JSON.parse(read('../translations/ko.json'))
|
|
619
619
|
assert.match(ko['figure.text.size-declared-to-follow'], /\{x\} ร \{y\} ร \{z\} mm/)
|
|
620
620
|
})
|
|
621
|
+
|
|
622
|
+
/* ํต โ ์ ๋น ์์๋ฅผ ํ๋ ํธ์์, ์ธ ์ถ ๋ชจ๋ ํฌ๊ธฐ๋ฅผ ๋ฐ๋ฅด๊ณ , ์์ก์ด๋ก ํค์ฐ๊ณ , ์์ชฝ ๋ฉด์ ๋ถ์ธ๋ค (G4, 09-26). */
|
|
623
|
+
test('ํ๋ ํธ์ ํต์ ํต์ ํฌ๊ธฐ๋ก ์๊ณ , ์ธ ์ถ ๋ชจ๋ ํฌ๊ธฐ๋ฅผ ๋ฐ๋ฅผ ์ ์๊ณ , ์์ก์ด๋ก ํค์ธ ์ ์๋ค', async () => {
|
|
624
|
+
const { paletteRecipe } = await import('../client/modeller/v3-palette.ts')
|
|
625
|
+
const { gizmoReachOf } = await import('../client/modeller/v3-gizmo.ts')
|
|
626
|
+
const { addV3Primitive: add } = await import('@hatiolab/figure-model')
|
|
627
|
+
const asset = add(createV3Asset('Tote'), paletteRecipe('hollow-box' as never, 'tote'))
|
|
628
|
+
assert.deepEqual(followableAxes(asset, 'tote'), ['x', 'y', 'z'])
|
|
629
|
+
assert.deepEqual(gizmoReachOf(asset as never, 'tote').sizes, ['tote.dimension.width', 'tote.dimension.height', 'tote.dimension.depth'])
|
|
630
|
+
const ko = JSON.parse(read('../translations/ko.json'))
|
|
631
|
+
assert.equal(ko['figure.label.face-inside-bottom'], '์์ชฝ ๋ฐ๋ฅ')
|
|
632
|
+
})
|
package/test/v3-recipes.test.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* **์กฐ๊ฐ
|
|
2
|
+
* **์กฐ๊ฐ ์นดํ๋ก๊ทธ** (ADR-0095, 2026-09-26). ํ
ํ๋ฆฟ์ ์๋ฒ์ ์ ์ ํ์ผ์ด๊ณ , ๋ชจ๋ธ๋ฌ๋ ๋ถ๋ฅ ยท ์ฐพ๊ธฐ ยท ์ค๋ช
ยท ๋ฃ๊ธฐ๋ฅผ ํ ์ฐฝ์์ ํ๋ค.
|
|
3
|
+
* ๋ถํ ์์ ์๋ ๋ฐฉ์์ ๋ ์ํผ์์ ์
ํ๋ค โ ๊ธ๋ก ์ ์ง ์๋๋ค.
|
|
3
4
|
*/
|
|
4
5
|
import { test } from 'node:test'
|
|
5
6
|
import assert from 'node:assert/strict'
|
|
6
7
|
import { readFileSync } from 'node:fs'
|
|
7
8
|
import { fileURLToPath } from 'node:url'
|
|
8
9
|
|
|
9
|
-
import { recipeFieldOf, recipeReady,
|
|
10
|
+
import { recipeFieldOf, recipeReady, recipesMatching, recipeValueOf, recipeWord, recipeWorkings, templatesOf } from '../client/modeller/v3-recipes.ts'
|
|
10
11
|
|
|
11
12
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
13
|
+
/* The product's templates, as the server reads them. */
|
|
14
|
+
const template = (name: string) => read(`../../figure-service/recipes/${name}/recipe.json`)
|
|
12
15
|
const share = { name: 'share', kind: 'ratio' as const, default: 0.35, min: 0.05, max: 0.95 }
|
|
13
16
|
|
|
14
17
|
test('๋น์จ์ ๋ฐฑ๋ถ์จ๋ก ๋ณด์ด๊ณ ๋ฐ๋๋ค; ๋น ์นธ์ ๊ธฐ๋ณธ๊ฐ; ์ ํ์ ์ ํ์ง ์ค์์๋ง', () => {
|
|
@@ -27,34 +30,53 @@ test('๋ง์ ํ๋ฉด์ ์ธ์ด๋ก, ์์ผ๋ฉด ์์ด๋ก; ๋ถํ์ฉ ์กฐ๊ฐ์ ๋ถ
|
|
|
27
30
|
assert.equal(recipeWord({ ko: '๋ฏธ๋ซ์ด ๋ฌธ', en: 'sliding door' }, 'ja'), 'sliding door')
|
|
28
31
|
assert.equal(recipeReady({ target: 'part' }, ''), false)
|
|
29
32
|
assert.equal(recipeReady({ target: 'part' }, 'room'), true)
|
|
30
|
-
|
|
31
|
-
assert.deepEqual(
|
|
33
|
+
const row = { category: 'door', tags: [], target: 'part' as const, title: 't', description: 'd', hasThumbnail: false }
|
|
34
|
+
assert.deepEqual(templatesOf([{ ...row, name: 'a', source: template('sliding-door') }, { ...row, name: 'b', source: 'not json' }]).map(t => t.name), ['a'])
|
|
32
35
|
})
|
|
33
36
|
|
|
34
|
-
test('
|
|
37
|
+
test('๋ถ๋ฅ์ ๋ฑ๋ง๋ก ์ฐพ๋๋ค โ ์ ๋ชฉ ยท ์ค๋ช
ยท ์ด๋ฆ ยท ํ๊ทธ ์ด๋์ ์๋ , ๋ฑ๋ง์ด ๋ชจ๋ ์์ด์ผ', () => {
|
|
38
|
+
const list = [
|
|
39
|
+
{ name: 'sliding-door', category: 'door', title: '๋ฏธ๋ซ์ด ๋ฌธ', description: '์๋ฉด์ ๋ฌธํ๊ณผ ๋ฌธ์ง', tags: ['door', 'lamp'] },
|
|
40
|
+
{ name: 'roller-row', category: 'conveyor', title: '๋กค๋ฌ ์ค', description: '์๋ฉด์ ๋กค๋ฌ', tags: ['roller'] }
|
|
41
|
+
]
|
|
42
|
+
assert.deepEqual(recipesMatching(list, '', '').map(t => t.name), ['sliding-door', 'roller-row'])
|
|
43
|
+
assert.deepEqual(recipesMatching(list, 'conveyor', '').map(t => t.name), ['roller-row'])
|
|
44
|
+
assert.deepEqual(recipesMatching(list, '', 'lamp ๋ฌธ').map(t => t.name), ['sliding-door'])
|
|
45
|
+
assert.deepEqual(recipesMatching(list, 'door', 'roller'), [])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('์๋ ๋ฐฉ์์ ๋ ์ํผ์์ ์
ํ๋ค โ ๋ฏธ๋ซ์ด ๋ฌธ: ๋ ๋ฌธ์ง์ด ๋ฏธ๋๋ฌ์ง๊ณ ํ๋๋ ๋ฐ๋ผ๊ฐ๋ฉฐ, ์กฐ์๊ฐ ํ๋, ๋จํ๋ ์กฐ์๊ฐ์ ๋ฐ๋ผ ๋ณด์', () => {
|
|
49
|
+
const door = JSON.parse(template('sliding-door'))
|
|
50
|
+
assert.deepEqual(recipeWorkings(door), [
|
|
51
|
+
{ key: 'figure.text.recipe-slides', words: { part: 'left' } },
|
|
52
|
+
{ key: 'figure.text.recipe-moves-with', words: { part: 'right', leader: 'left' } },
|
|
53
|
+
{ key: 'figure.text.recipe-controls', words: { count: 1 } },
|
|
54
|
+
{ key: 'figure.text.recipe-shown-by-control', words: { parts: 'lamp' } }
|
|
55
|
+
])
|
|
56
|
+
/* The rollers spin on one control, every copy (ADR-0093, 2026-09-26). */
|
|
57
|
+
assert.deepEqual(recipeWorkings(JSON.parse(template('roller-row'))), [
|
|
58
|
+
{ key: 'figure.text.recipe-turns', words: { part: 'roller' } },
|
|
59
|
+
{ key: 'figure.text.recipe-controls', words: { count: 1 } },
|
|
60
|
+
{ key: 'figure.text.recipe-row', words: { part: 'roller' } }
|
|
61
|
+
])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('๋ชจ๋ธ๋ฌ โ ๋ถํ ์นธ์ ๋จ์ถ๊ฐ ์นดํ๋ก๊ทธ๋ฅผ ์ด๊ณ , ๋ฃ์ผ๋ฉด ํ ๋จ๊ณ๋ก ๋ค์ด๊ฐ๊ณ ์ฐฝ์ด ๋ซํ๋ค; ๋ชฉ๋ก์ ํ์ด์ง๊ฐ ์๋ฒ์์ ํ ๋ฒ ์ฝ๋๋ค', () => {
|
|
65
|
+
const parts = read('../client/modeller/figure-v3-parts.ts')
|
|
66
|
+
assert.ok(parts.includes("this.ask('open-recipe-catalog', {})"))
|
|
35
67
|
const editor = read('../client/modeller/v3-editor.ts')
|
|
36
68
|
const insert = editor.slice(editor.indexOf(' private insertRecipe('), editor.indexOf(' /** The design values of the asset being edited. */'))
|
|
37
|
-
assert.ok(insert.includes('v3RecipeIdFor(asset, checked)') && insert.includes('
|
|
38
|
-
assert.ok(insert.includes('this.
|
|
39
|
-
assert.ok(insert.includes('this.recipeError = error instanceof Error ? error.message : String(error)'), '๊ฑฐ์ ์ ๊ทธ๋๋ก')
|
|
40
|
-
const session = read('../client/modeller/v3-product-session.ts')
|
|
41
|
-
assert.ok(session.includes("workspace.applyProposal({ origin: 'ui', context: at.context, expectedRevision: at.expectedRevision, steps })"), '์ฌ๋์ด ๋ฃ์ ๊ฒ')
|
|
69
|
+
assert.ok(insert.includes('v3RecipeIdFor(asset, checked)') && insert.includes('this.session!.applyRecipe(steps)') && insert.includes('this.catalogOpen = false'))
|
|
70
|
+
assert.ok(insert.includes('this.recipeError = error instanceof Error ? error.message : String(error)'), '๊ฑฐ์ ์ ์ปค๋์ ๋ง๋ก')
|
|
42
71
|
const page = read('../client/pages/figure-v3-modeller-page.ts')
|
|
43
|
-
assert.ok(page.includes(
|
|
72
|
+
assert.ok(page.includes("availableRecipes(i18next.language || 'en')") && page.includes('.templates=${this.templates}'))
|
|
73
|
+
const catalog = read('../client/modeller/figure-v3-recipe-catalog.ts')
|
|
74
|
+
assert.ok(catalog.includes('describeV3Recipe(t.recipe)'), '๋ถํ ์๋ ์
ํ๋ค')
|
|
75
|
+
assert.ok(catalog.includes("${this.guide ? html`<ox-markdown"), '์ค๋ช
์ด ์์ผ๋ฉด ์๋ฌด๊ฒ๋ ๋์ง ์๋๋ค โ ใ์ค๋น ์คใ ์์')
|
|
44
76
|
})
|
|
45
77
|
|
|
46
|
-
test('
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
assert.
|
|
50
|
-
assert.equal(recipeIsOwn({ domain: { subdomain: 'plant-1' } }, 'plant-1'), true)
|
|
51
|
-
assert.equal(recipeIsOwn({ domain: { subdomain: 'system' } }, 'plant-1'), false)
|
|
52
|
-
assert.equal(recipeIsOwn({ domain: { subdomain: 'system' } }, ''), false, '์ฃผ์๊ฐ ๋๋ฉ์ธ์ ๋งํ์ง ์์ผ๋ฉด ์ ๊ฒ์ด ์๋๋ค')
|
|
53
|
-
assert.equal(paramRangeOf(share), '5 โ 95 %')
|
|
54
|
-
assert.equal(paramRangeOf({ name: 'leaves', kind: 'choice', default: 2, options: [1, 2] }), '1 / 2')
|
|
55
|
-
const page = read('../client/pages/figure-recipe-page.ts')
|
|
56
|
-
assert.ok(page.includes('applyV3Recipe(room.asset, parseV3Recipe(recipe)'), '๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ ๊ฒ์ฌํ๋ ๋ฐฉ์ ๋ฃ์ ๋ชจ์ต')
|
|
57
|
-
assert.ok(page.includes("saveFigureRecipe(row.source, 'draft')"), '์์ ๋๋ฉ์ธ ๊ฒ์ ์ด์์ผ๋ก ๋ณต์ฌํด ๊ณ ์น๋ค')
|
|
58
|
-
assert.ok(page.includes('OxPrompt.open('), '์ง์ฐ๊ธฐ ์ ์ ์ฑ์ ํ์ธ ์ฐฝ์ผ๋ก ๋ฌป๋๋ค')
|
|
59
|
-
assert.ok(read('../things-factory.config.js').includes("{ tagname: 'figure-recipe-page', page: 'figure-recipes' }"))
|
|
78
|
+
test('๋ฃ๊ธฐ๋ ์นดํ๋ก๊ทธ์์ ์จ๋ค โ ํธ์ง๊ธฐ๋ ์นดํ๋ก๊ทธ ์์์์ insert-recipe ๋ฅผ ๋ฃ๋๋ค (์๋ชป ์ฎ๊ฒจ์ง ์ฐ๊ฒฐ, 09-26)', () => {
|
|
79
|
+
const editor = read('../client/modeller/v3-editor.ts')
|
|
80
|
+
const catalog = editor.slice(editor.indexOf('<figure-v3-recipe-catalog'), editor.indexOf('</figure-v3-recipe-catalog>'))
|
|
81
|
+
assert.ok(catalog.includes('@insert-recipe=${(e: CustomEvent) => this.insertRecipe(e.detail.recipe, e.detail.values)}'))
|
|
60
82
|
})
|
package/things-factory.config.js
CHANGED
|
@@ -5,7 +5,6 @@ export default {
|
|
|
5
5
|
route,
|
|
6
6
|
routes: [
|
|
7
7
|
{ tagname: 'figure-list-page', page: 'figure-list' },
|
|
8
|
-
{ tagname: 'figure-recipe-page', page: 'figure-recipes' },
|
|
9
8
|
/* One modeller since ADR-0087 step 5; the old V3 address is an alias. The shell builds the DOM from this table. */
|
|
10
9
|
{ tagname: 'figure-v3-modeller-page', page: 'figure-modeller' },
|
|
11
10
|
{ tagname: 'figure-v3-modeller-page', page: 'figure-v3-modeller' }
|