@things-factory/figure-ui 10.1.175 → 10.1.177
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/modeller/editor-styles.ts +4 -6
- package/client/modeller/editor.ts +41 -39
- package/client/modeller/figure-links.ts +41 -12
- package/client/modeller/figure-parts.ts +31 -14
- package/client/modeller/figure-properties.ts +2 -2
- package/client/pages/figure-modeller-page.ts +34 -11
- package/dist-client/modeller/editor-styles.js +4 -6
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +6 -10
- package/dist-client/modeller/editor.js +34 -37
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +7 -2
- package/dist-client/modeller/figure-links.js +45 -11
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-parts.js +31 -14
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-properties.js +2 -2
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.js +34 -11
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/test/browser/acceptance-page.js +2 -1
- package/test/browser/build.mjs +9 -1
- package/test/browser/editing-page.js +2 -1
- package/test/browser/links.check.mjs +24 -9
- package/test/browser/modeller.check.mjs +6 -0
- package/test/browser/parts.check.mjs +3 -12
- package/test/browser/places-mesh.check.mjs +4 -9
- package/test/browser/props.check.mjs +67 -12
- package/test/browser/row-preview.check.mjs +4 -0
- package/test/browser/screen.check.mjs +49 -19
- package/test/browser/serve-acceptance.mjs +2 -1
- package/test/place-words.test.ts +31 -0
- package/translations/en.json +6 -3
- package/translations/es.json +3 -0
- package/translations/ja.json +3 -0
- package/translations/ko.json +6 -3
- package/translations/ms.json +3 -0
- package/translations/zh.json +3 -0
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.177",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"@operato/slot-layout-editor": "^10.29.25",
|
|
42
42
|
"@operato/styles": "^10.0.0",
|
|
43
43
|
"@operato/utils": "^10.0.0",
|
|
44
|
-
"@things-factory/ai-assistant": "^10.1.
|
|
45
|
-
"@things-factory/figure-service": "^10.1.
|
|
46
|
-
"@things-factory/figure-viewer": "^10.1.
|
|
47
|
-
"@things-factory/setting-base": "^10.1.
|
|
44
|
+
"@things-factory/ai-assistant": "^10.1.177",
|
|
45
|
+
"@things-factory/figure-service": "^10.1.177",
|
|
46
|
+
"@things-factory/figure-viewer": "^10.1.176",
|
|
47
|
+
"@things-factory/setting-base": "^10.1.176",
|
|
48
48
|
"three": "^0.185.1"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "91da09c945ec5e77ab960074db8f53b9ea8ac534"
|
|
51
51
|
}
|
|
@@ -214,7 +214,8 @@ async function start() {
|
|
|
214
214
|
}
|
|
215
215
|
const properties = document.createElement('figure-properties')
|
|
216
216
|
properties.slot = 'figure'
|
|
217
|
-
|
|
217
|
+
/* As the modeller page gives it: the revision on screen (it gave the source, which the panel no longer reads -- N6). */
|
|
218
|
+
const keep = () => (properties.revision = editor.session.revisionNow())
|
|
218
219
|
keep()
|
|
219
220
|
editor.addEventListener('changed', keep)
|
|
220
221
|
/* The figure panel's commands, as the modeller page runs them: one step of the session's history. */
|
package/test/browser/build.mjs
CHANGED
|
@@ -40,6 +40,14 @@ for (const signal of ['SIGINT', 'SIGTERM'])
|
|
|
40
40
|
* `standIns` maps a module the page would import to a file beside this one: for a module that cannot run here (the
|
|
41
41
|
* assistant dock needs the whole app's tool bar), a stand-in that records what the product asked of it.
|
|
42
42
|
*/
|
|
43
|
+
/*
|
|
44
|
+
Packages that must stand once on a page: what defines custom elements (lit, the @operato and @material elements) and
|
|
45
|
+
what holds one state (i18next). A sibling checkout linked into node_modules for local work -- the operato
|
|
46
|
+
webcomponents, 2026-10-17 -- brought its own @operato/i18n, the page defined ox-i18n twice and stopped before any
|
|
47
|
+
check could start. Each is taken from this checkout's node_modules, whoever imports it.
|
|
48
|
+
*/
|
|
49
|
+
const ONE_COPY = id => /^(lit|lit-html|lit-element|@lit\/|@lit-labs\/|i18next|@operato\/|@material\/)/.test(id)
|
|
50
|
+
|
|
43
51
|
export async function buildPage(page = 'modeller-page.js', { standIns = {} } = {}) {
|
|
44
52
|
/*
|
|
45
53
|
The real path: on macOS the temp dir is a link (/var -> /private/var). A module named both through
|
|
@@ -87,7 +95,7 @@ export async function buildPage(page = 'modeller-page.js', { standIns = {} } = {
|
|
|
87
95
|
}
|
|
88
96
|
},
|
|
89
97
|
replace({ preventAssignment: true, values: { 'process.env.NODE_ENV': JSON.stringify('production') } }),
|
|
90
|
-
nodeResolve({ browser: true, modulePaths: [join(PACKAGE, 'node_modules'), join(ROOT, 'node_modules')] }),
|
|
98
|
+
nodeResolve({ browser: true, modulePaths: [join(PACKAGE, 'node_modules'), join(ROOT, 'node_modules')], rootDir: PACKAGE, dedupe: ONE_COPY }),
|
|
91
99
|
commonjs()
|
|
92
100
|
]
|
|
93
101
|
})
|
|
@@ -94,7 +94,8 @@ window.editing = {
|
|
|
94
94
|
},
|
|
95
95
|
/** The labels of the number fields on the properties tab. */
|
|
96
96
|
propertyInputs() {
|
|
97
|
-
|
|
97
|
+
/* The panel shown: the properties, or the 「반응형」 tab a check pressed. */
|
|
98
|
+
const panel = editor().renderRoot.querySelector('div[panel]:not([off])')
|
|
98
99
|
return [...panel.querySelectorAll('input')].map(i => i.getAttribute('aria-label'))
|
|
99
100
|
},
|
|
100
101
|
/** The page point of the first element a selector finds -- anywhere on the page, or inside the `host` elements -- and what it says. */
|
|
@@ -90,11 +90,6 @@ async function pick(part) {
|
|
|
90
90
|
await until(id => probe.state().selected === id, [part], `${part} selected`)
|
|
91
91
|
}
|
|
92
92
|
const tab = name => read(l => probe.tab(l), `figure.label.tab-short-${name}`).then(point => click(point, `the ${name} tab`))
|
|
93
|
-
/** What the figure does is a folded group of the properties (chief architect 2026-10-16): its fold opened, as a person opens it. */
|
|
94
|
-
async function openCapability() {
|
|
95
|
-
const summary = await handle('section[capability] > details > summary')
|
|
96
|
-
if (!(await summary.evaluate(s => s.parentElement.open))) await summary.click()
|
|
97
|
-
}
|
|
98
93
|
/** A fastening the part has is said in one line; 「바꾸기」 opens its drawing and details. */
|
|
99
94
|
async function changeFastening() {
|
|
100
95
|
await until(() => look.all('button[change-fastening]').length === 1, [], 'the fastening said in one line')
|
|
@@ -131,6 +126,12 @@ async function press(selector, text, what = text ?? selector) {
|
|
|
131
126
|
text
|
|
132
127
|
)
|
|
133
128
|
assert.ok(await element.evaluate(e => !!e), `${what} is on the page`)
|
|
129
|
+
/*
|
|
130
|
+
In the middle of the panel first, as a hand scrolls to what it presses: brought only just into view, at the panel's
|
|
131
|
+
top, it lay under the panel's sticky head and the press fell there (2026-10-17, the leaf's fasten-start once the
|
|
132
|
+
rows were tightened).
|
|
133
|
+
*/
|
|
134
|
+
await element.evaluate(e => e.scrollIntoView({ block: 'center' }))
|
|
134
135
|
await element.click()
|
|
135
136
|
await settled()
|
|
136
137
|
}
|
|
@@ -205,6 +206,8 @@ describe('a part made to follow the figure size, and let go of it', () => {
|
|
|
205
206
|
a = applyFigureAuthoring(a, { kind: 'declare-instance-size', range: SIZE, at: { x: 400, y: 400, z: 400 } })
|
|
206
207
|
await mount('sized', applyFigureAuthoring(a, { kind: 'link-dimension', part: 'box', axis: 'x', source: { of: 'instance', axis: 'x' } }))
|
|
207
208
|
await pick('box')
|
|
209
|
+
/* How a part answers a size is the 「반응형」 tab's (user 2026-10-17). */
|
|
210
|
+
await tab('response')
|
|
208
211
|
})
|
|
209
212
|
|
|
210
213
|
test('on a figure with a size, follow-size is one step, says nothing of a size declared, and is not offered again', async () => {
|
|
@@ -247,6 +250,8 @@ function sizedPair(turning) {
|
|
|
247
250
|
describe('the sizes a figure may be placed at, and how it grows', () => {
|
|
248
251
|
test('measured again, the declared range becomes the measured one in one step, and the offer goes', async () => {
|
|
249
252
|
await mount('sized-pair', sizedPair(false))
|
|
253
|
+
/* The figure's range is the 「반응형」 tab's. */
|
|
254
|
+
await tab('response')
|
|
250
255
|
const start = await state()
|
|
251
256
|
await click(await at('button', 'figure.button.remeasure-range'), 'remeasure')
|
|
252
257
|
await oneStep(start)
|
|
@@ -264,6 +269,7 @@ describe('the sizes a figure may be placed at, and how it grows', () => {
|
|
|
264
269
|
|
|
265
270
|
test('a figure that turns about a joint is urged to grow by one ratio', async () => {
|
|
266
271
|
await mount('turning', sizedPair(true))
|
|
272
|
+
await tab('response')
|
|
267
273
|
assert.equal((await all('p[hint]', 'figure.text.sizing-uniform-proposed')).length, 1)
|
|
268
274
|
})
|
|
269
275
|
})
|
|
@@ -279,6 +285,8 @@ describe('a row\'s pitch', () => {
|
|
|
279
285
|
test('a pitch that follows the frame\'s length shows as the number it comes to, and the row is said as drawn', async () => {
|
|
280
286
|
await mount('pitch-follows', applyFigureAuthoring(conveyor(), { kind: 'set-repeat', part: 'roller', pitch: { from: { of: 'part', part: 'frame', axis: 'x' }, times: 0.1 } }))
|
|
281
287
|
await pick('roller')
|
|
288
|
+
/* The row is the 「반응형」 tab's. */
|
|
289
|
+
await tab('response')
|
|
282
290
|
assert.deepEqual((await all('input[aria-label="roller.repeat.pitch"]')).map(i => i.value), ['200'], 'a tenth of 2000')
|
|
283
291
|
assert.ok((await all('ul[row-reading] li')).length > 0, 'the row said as it stands')
|
|
284
292
|
})
|
|
@@ -291,6 +299,7 @@ describe('a row\'s pitch', () => {
|
|
|
291
299
|
])
|
|
292
300
|
await mount('published-pitch', published)
|
|
293
301
|
await pick('roller')
|
|
302
|
+
await tab('response')
|
|
294
303
|
const start = await state()
|
|
295
304
|
await type(await handle('input[aria-label="roller.repeat.pitch"]'), '300')
|
|
296
305
|
await oneStep(start)
|
|
@@ -304,7 +313,8 @@ describe('holding places on a part', () => {
|
|
|
304
313
|
test('a place needs kinds it takes; a figure that holds nothing says so, and declaring it holds things is one step', async () => {
|
|
305
314
|
await mount('box')
|
|
306
315
|
await pick('box')
|
|
307
|
-
|
|
316
|
+
/* What the figure does is a tab again (user 2026-10-17). */
|
|
317
|
+
await tab('capability')
|
|
308
318
|
assert.equal((await at('button[act]', 'figure.button.add-place'))?.disabled, true, 'no kinds: no place')
|
|
309
319
|
await type(await handle('input[placeholder="BOX, TOTE"]'), 'BOX')
|
|
310
320
|
assert.equal((await at('button[act]', 'figure.button.add-place'))?.disabled, false, 'a kind: a place may be made')
|
|
@@ -327,6 +337,8 @@ describe('a position worked out from the figure size', () => {
|
|
|
327
337
|
a.document.model.nodes.find(n => n.id === 'lamp.pose').args[1] = 'lamp.lift.value'
|
|
328
338
|
await mount('lifted', a)
|
|
329
339
|
await pick('lamp')
|
|
340
|
+
/* A position that follows the figure size is the 「반응형」 tab's. */
|
|
341
|
+
await tab('response')
|
|
330
342
|
assert.equal((await all('div[pair] span', 'figure.label.position-worked-out')).length, 1)
|
|
331
343
|
const start = await state()
|
|
332
344
|
await click(await at('button[act]', 'figure.button.scale-position'), 'scale position')
|
|
@@ -525,13 +537,15 @@ describe('a bench: what each part is fastened to, what detaching would do, a row
|
|
|
525
537
|
|
|
526
538
|
test('a part fastened to nothing: the row is folded under its head and says why', async () => {
|
|
527
539
|
await pick('loose')
|
|
540
|
+
await tab('response')
|
|
528
541
|
assert.deepEqual((await all('details[row-fold] > summary h3')).map(h => h.text), ['figure.label.repeat'])
|
|
529
542
|
assert.deepEqual((await all('details[row-fold]')).map(d => d.open), [false], 'folded')
|
|
530
543
|
assert.deepEqual((await all('p[row-blocked]')).map(p => p.text), ['figure.text.row-blocked-unfastened'])
|
|
531
544
|
})
|
|
532
545
|
|
|
533
|
-
test('the row worked on is the one seen: its copies drawn and said;
|
|
546
|
+
test('the row worked on is the one seen: its copies drawn and said; on the properties tab they are not', async () => {
|
|
534
547
|
await pick('lamp')
|
|
548
|
+
await tab('response')
|
|
535
549
|
assert.deepEqual((await all('details[row-fold]')).map(d => d.open), [false], 'folded')
|
|
536
550
|
await read(() => look.hear('preview-command'))
|
|
537
551
|
/* One click opens it, and it stays open while the row is worked. */
|
|
@@ -543,8 +557,9 @@ describe('a bench: what each part is fastened to, what detaching would do, a row
|
|
|
543
557
|
assert.equal((await heard('preview-command')).filter(Boolean).at(-1).action.kind, 'make-repeat')
|
|
544
558
|
await until(() => look.drawn().filter(n => n.startsWith('__preview')).length > 1, [], 'the copies drawn')
|
|
545
559
|
await until(() => look.all('section[row] [preview-words]', 'figure.text.row-count-pitch').length === 1, [], 'the row said')
|
|
546
|
-
|
|
547
|
-
await
|
|
560
|
+
/* The properties tab shown: what its fastening would do is said, the row's copies are not drawn. */
|
|
561
|
+
await tab('properties')
|
|
562
|
+
await until(() => !look.drawn().some(n => n.startsWith('__preview')), [], 'the copies not drawn on the properties tab')
|
|
548
563
|
})
|
|
549
564
|
})
|
|
550
565
|
|
|
@@ -53,6 +53,8 @@ async function click(point) {
|
|
|
53
53
|
await page().mouse.click(point.x, point.y)
|
|
54
54
|
await sleep(400)
|
|
55
55
|
}
|
|
56
|
+
/* A side tab pressed, by its short name; how a part answers a size is the 「반응형」 tab's (user 2026-10-17). */
|
|
57
|
+
const tab = async name => click(await read(l => probe.tab(l), `figure.label.tab-short-${name}`))
|
|
56
58
|
|
|
57
59
|
const KEYS = { translate: 'KeyW', rotate: 'KeyE', scale: 'KeyR' }
|
|
58
60
|
|
|
@@ -383,6 +385,7 @@ describe('the fastening form belongs to the part it is for', () => {
|
|
|
383
385
|
describe('a figure made in the modeller is given its size from its parts (user 2026-09-25)', () => {
|
|
384
386
|
before(async () => {
|
|
385
387
|
await mount('box')
|
|
388
|
+
await tab('response')
|
|
386
389
|
})
|
|
387
390
|
|
|
388
391
|
test('with nothing picked, the figure offers to declare its size; clicked, its size fields appear', async () => {
|
|
@@ -697,6 +700,7 @@ describe('a part follows the figure size in one step (follow-size, 2026-09-26)',
|
|
|
697
700
|
test('one click on a figure with no size: its size is declared from the part, said in a line, and the part follows it', async () => {
|
|
698
701
|
await mount('box')
|
|
699
702
|
await click(await read(() => probe.row('box')))
|
|
703
|
+
await tab('response')
|
|
700
704
|
const at = await page().evaluate(() => {
|
|
701
705
|
const deep = (root = document) => [...root.querySelectorAll('button[follow-size]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
702
706
|
const b = deep()[0]
|
|
@@ -827,6 +831,7 @@ describe('a row\'s pitch made a board\'s to set (ADR-0096)', () => {
|
|
|
827
831
|
test('published in one step with a name and a range; the pitch then sets the input; withdrawn, the pitch stays as it is', async () => {
|
|
828
832
|
await mount('rollers')
|
|
829
833
|
await click(await read(() => probe.row('roller')))
|
|
834
|
+
await tab('response')
|
|
830
835
|
const before = (await read(() => probe.ids())).filter(id => id.includes('"roller.placed"')).length
|
|
831
836
|
await click(await read(() => probe.labelled('figure.label.board-sets')))
|
|
832
837
|
await typeInto('figure.label.board-name', '롤러 간격')
|
|
@@ -893,6 +898,7 @@ describe('the row section opens on one click', () => {
|
|
|
893
898
|
test('a part that can be made a row: one click on the folded heading opens it, a second folds it', async () => {
|
|
894
899
|
await mount('rollers')
|
|
895
900
|
await click(await read(() => probe.row('mark')))
|
|
901
|
+
await tab('response')
|
|
896
902
|
const summary = await page().evaluateHandle(() => {
|
|
897
903
|
const deep = (root = document) => [...root.querySelectorAll('details[row-fold] > summary')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
898
904
|
return deep()[0] ?? null
|
|
@@ -320,7 +320,8 @@ describe('a row is one part: selected whole, its pitch one value set by the row
|
|
|
320
320
|
|
|
321
321
|
test('the pitch is offered once, and typed it is one step that sets the row again', async () => {
|
|
322
322
|
await pick('roller')
|
|
323
|
-
|
|
323
|
+
/* The row's pitch is the 「반응형」 tab's (user 2026-10-17). */
|
|
324
|
+
await tab('response')
|
|
324
325
|
const fields = await read(() => editing.propertyInputs())
|
|
325
326
|
assert.equal(fields.filter(label => /pitch|repeat/i.test(label ?? '')).length, 1, fields.join())
|
|
326
327
|
const label = fields.find(label => /pitch|repeat/i.test(label ?? ''))
|
|
@@ -344,18 +345,8 @@ describe('the long explanations open from their 「?」 beside the title, closed
|
|
|
344
345
|
await mount('box', 'doors')
|
|
345
346
|
await pick('left')
|
|
346
347
|
const seen = new Set()
|
|
347
|
-
for (const name of ['properties', 'motion']) {
|
|
348
|
+
for (const name of ['properties', 'response', 'motion', 'capability']) {
|
|
348
349
|
await tab(name)
|
|
349
|
-
/* What the figure does is a folded group of the properties now (chief architect 2026-10-16): opened as a hand opens it. */
|
|
350
|
-
if (name === 'properties') {
|
|
351
|
-
const fold = await read(() => {
|
|
352
|
-
const summary = document.querySelector('figure-editor').renderRoot.querySelector('div[panel]:not([off]) section[capability] > details > summary')
|
|
353
|
-
summary.scrollIntoView({ block: 'center' })
|
|
354
|
-
const r = summary.getBoundingClientRect()
|
|
355
|
-
return { x: r.left + 20, y: r.top + r.height / 2 }
|
|
356
|
-
})
|
|
357
|
-
await click(fold, 'the capability fold')
|
|
358
|
-
}
|
|
359
350
|
const helps = await read(() =>
|
|
360
351
|
[...document.querySelector('figure-editor').renderRoot.querySelectorAll('figure-links, figure-clips')].flatMap((element, host) =>
|
|
361
352
|
[...element.shadowRoot.querySelectorAll('button[help]')].map((b, i) => ({ host, i, seen: b.getClientRects().length > 0 })).filter(b => b.seen)
|
|
@@ -198,15 +198,10 @@ describe('places made by a rule: the shared slot layout editor on a part\'s face
|
|
|
198
198
|
before(async () => {
|
|
199
199
|
await mount(addFigureBox(figure('Stacker'), { id: 'fork', size: { x: 1200, y: 50, z: 900 }, position: { x: 0, y: 25, z: 0 } }))
|
|
200
200
|
await pick('fork')
|
|
201
|
-
/* What the figure does is a
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const r = summary.getBoundingClientRect()
|
|
206
|
-
return { x: r.left + 20, y: r.top + r.height / 2 }
|
|
207
|
-
})
|
|
208
|
-
await page().mouse.click(fold.x, fold.y)
|
|
209
|
-
await until(() => !!document.querySelector('figure-editor').renderRoot.querySelector('div[panel]:not([off]) section[capability] > details[open]'), [], 'the capability fold open')
|
|
201
|
+
/* What the figure does is a tab of its own (user 2026-10-17). */
|
|
202
|
+
const tab = await read(l => probe.tab(l), 'figure.label.tab-short-capability')
|
|
203
|
+
await page().mouse.click(tab.x, tab.y)
|
|
204
|
+
await until(() => document.querySelector('figure-editor').shadowRoot.querySelector('button[tab][on]')?.getAttribute('aria-label') === 'figure.label.tab-short-capability', [], 'the capability tab on')
|
|
210
205
|
await settled()
|
|
211
206
|
})
|
|
212
207
|
|
|
@@ -70,8 +70,8 @@ async function press(selector, what) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
* The groups of the panel shown, top to bottom, by what each is: the part's name, the size
|
|
74
|
-
*
|
|
73
|
+
* The groups of the panel shown, top to bottom, by what each is: the part's name, the size and the place the links
|
|
74
|
+
* draw, the look and 「더 보기」 on the properties tab; on 「반응형」 the size rules, a position that follows, the row.
|
|
75
75
|
*/
|
|
76
76
|
const groupsInOrder = () =>
|
|
77
77
|
read(() => {
|
|
@@ -80,11 +80,11 @@ const groupsInOrder = () =>
|
|
|
80
80
|
const links = panel.querySelector('figure-links')?.shadowRoot
|
|
81
81
|
const marks = {
|
|
82
82
|
name: panel.querySelector('input[name-field]'),
|
|
83
|
-
size: links?.querySelector('section[size]'),
|
|
83
|
+
size: links?.querySelector('section[size], section[size-response]'),
|
|
84
84
|
place: links?.querySelector('section[fastening]'),
|
|
85
|
+
position: links?.querySelector('section[position-response]'),
|
|
85
86
|
row: panel.querySelector('section[slot=row]') ?? links?.querySelector('section[row]'),
|
|
86
87
|
look: panel.querySelector('figure-material-panel:not([show])'),
|
|
87
|
-
capability: panel.querySelector('section[capability]'),
|
|
88
88
|
more: panel.querySelector('section[more]')
|
|
89
89
|
}
|
|
90
90
|
return Object.entries(marks)
|
|
@@ -93,12 +93,18 @@ const groupsInOrder = () =>
|
|
|
93
93
|
.map(([name]) => name)
|
|
94
94
|
})
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
/* A side tab pressed, by its name. */
|
|
97
|
+
async function tab(name) {
|
|
98
|
+
await press(`aside button[tab][aria-label="figure.label.tab-short-${name}"]`, `the ${name} tab`)
|
|
99
|
+
await read(() => document.querySelector('figure-editor').updateComplete)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
describe('the side: six tabs, the view and the history from the bar', () => {
|
|
97
103
|
before(() => mount('rollers'))
|
|
98
104
|
|
|
99
|
-
test('properties, motion, data and the verdict -- the
|
|
105
|
+
test('properties, responsive, motion, capability, data and the verdict -- the six tabs, in that order (user 2026-10-17)', async () => {
|
|
100
106
|
const tabs = await read(() => [...document.querySelector('figure-editor').shadowRoot.querySelectorAll('aside button[tab]')].map(b => b.getAttribute('aria-label')))
|
|
101
|
-
assert.deepEqual(tabs, ['properties', 'motion', 'data', 'verdict'].map(t => `figure.label.tab-short-${t}`))
|
|
107
|
+
assert.deepEqual(tabs, ['properties', 'response', 'motion', 'capability', 'data', 'verdict'].map(t => `figure.label.tab-short-${t}`))
|
|
102
108
|
})
|
|
103
109
|
|
|
104
110
|
test('the view opens over the work from the bar, pressed while open; Esc closes it', async () => {
|
|
@@ -126,18 +132,27 @@ describe('the side: four tabs, the view and the history from the bar', () => {
|
|
|
126
132
|
describe('a part\'s groups in one order, whatever its state', () => {
|
|
127
133
|
before(() => mount('rollers'))
|
|
128
134
|
|
|
129
|
-
test('a part fastened in a row: name, size, place,
|
|
135
|
+
test('a part fastened in a row: name, size, place, look, more; on 「반응형」 its size rules and its row', async () => {
|
|
130
136
|
await pick('roller')
|
|
131
|
-
|
|
137
|
+
await tab('properties')
|
|
138
|
+
assert.deepEqual(await groupsInOrder(), ['name', 'size', 'place', 'look', 'more'])
|
|
139
|
+
await tab('response')
|
|
140
|
+
assert.deepEqual(await groupsInOrder(), ['size', 'row'])
|
|
141
|
+
await tab('properties')
|
|
132
142
|
})
|
|
133
143
|
|
|
134
144
|
test('a part on the plane, not in a row: the same order, the row folded where a row would be', async () => {
|
|
135
145
|
await pick('bed')
|
|
136
|
-
assert.deepEqual(await groupsInOrder(), ['name', 'size', 'place', '
|
|
146
|
+
assert.deepEqual(await groupsInOrder(), ['name', 'size', 'place', 'look', 'more'])
|
|
147
|
+
/* On the plane, its height is worked out from the figure size: the position group stands between. */
|
|
148
|
+
await tab('response')
|
|
149
|
+
assert.deepEqual(await groupsInOrder(), ['size', 'position', 'row'])
|
|
150
|
+
await tab('properties')
|
|
137
151
|
})
|
|
138
152
|
|
|
139
153
|
test('a fastened part\'s place is one line and 「바꾸기」: no locked cells, no drawing, until it is changed', async () => {
|
|
140
154
|
await pick('mark')
|
|
155
|
+
await tab('properties')
|
|
141
156
|
const place = () =>
|
|
142
157
|
read(() => {
|
|
143
158
|
const editor = document.querySelector('figure-editor')
|
|
@@ -187,6 +202,8 @@ describe('a group\'s 「⋯」: its rarer actions, by the mouse or the keyboard'
|
|
|
187
202
|
|
|
188
203
|
test('Esc closes the menu and nothing is done', async () => {
|
|
189
204
|
await pick('roller')
|
|
205
|
+
/* The row is the 「반응형」 tab's. */
|
|
206
|
+
await tab('response')
|
|
190
207
|
const start = await revision()
|
|
191
208
|
await press('section[row] button[group-menu]', 'the row\'s 「⋯」')
|
|
192
209
|
await until(() => document.activeElement?.localName === 'ox-popup-menu', [], 'the menu open, with the focus')
|
|
@@ -232,16 +249,19 @@ describe('the parts panel stands at the side\'s heights (2026-10-16)', () => {
|
|
|
232
249
|
before(() => mount('rollers'))
|
|
233
250
|
|
|
234
251
|
test('the detail level is one height with the side\'s fields, and the palette\'s tiles one height with each other', async () => {
|
|
252
|
+
await pick('bed')
|
|
235
253
|
const heights = await read(() => {
|
|
236
254
|
const parts = document.querySelector('figure-editor').shadowRoot.querySelector('figure-parts').shadowRoot
|
|
237
255
|
const h = e => Math.round(e.getBoundingClientRect().height)
|
|
238
256
|
const tiles = [...parts.querySelectorAll('div[palette] button[shape]:not([import])')].map(h)
|
|
239
257
|
return { level: h(parts.querySelector('div[level] select')), tiles: [...new Set(tiles)], count: tiles.length, import: h(parts.querySelector('button[shape][import]')) }
|
|
240
258
|
})
|
|
241
|
-
|
|
259
|
+
const side = await read(() => Math.round(document.querySelector('figure-editor').shadowRoot.querySelector('input[name-field]')?.getBoundingClientRect().height ?? 0))
|
|
260
|
+
assert.ok(side > 0, 'a side field to measure against')
|
|
261
|
+
assert.equal(heights.level, side, 'the detail level select, one height with the side\'s fields')
|
|
242
262
|
assert.ok(heights.count >= 9, `the starting shapes: ${heights.count}`)
|
|
243
263
|
assert.equal(heights.tiles.length, 1, `one tile height: ${heights.tiles}`)
|
|
244
|
-
assert.equal(heights.import,
|
|
264
|
+
assert.equal(heights.import, side, 'the file import on one line, its own row, at the fields\' height')
|
|
245
265
|
})
|
|
246
266
|
|
|
247
267
|
/* A static guard sees the sizes written down; an icon with none stands at md-icon's own 24 (the recipe catalog's did). */
|
|
@@ -260,3 +280,38 @@ describe('the parts panel stands at the side\'s heights (2026-10-16)', () => {
|
|
|
260
280
|
assert.deepEqual(sizes.filter(s => !['16', '18', '20'].includes(s.split(' ')[0])), [])
|
|
261
281
|
})
|
|
262
282
|
})
|
|
283
|
+
|
|
284
|
+
describe('a label and its field on one line, the field a line not a box (user 2026-10-17)', () => {
|
|
285
|
+
before(() => mount('rollers'))
|
|
286
|
+
|
|
287
|
+
/* Every visible label beside the control after it: the field's row, not the row above. */
|
|
288
|
+
const rows = () =>
|
|
289
|
+
read(() => {
|
|
290
|
+
const all = []
|
|
291
|
+
const walk = root => { for (const e of root.querySelectorAll('*')) { all.push(e); if (e.shadowRoot) walk(e.shadowRoot) } }
|
|
292
|
+
walk(document.querySelector('figure-editor').shadowRoot.querySelector('div[panel]:not([off])'))
|
|
293
|
+
const out = []
|
|
294
|
+
for (const field of all.filter(e => e.matches?.('div[field], div[inline-field], div[form]') && e.checkVisibility())) {
|
|
295
|
+
const label = field.querySelector(':scope > label, :scope > span')
|
|
296
|
+
const control = field.querySelector(':scope > input, :scope > select, :scope > div[swatches], :scope > div[row]')
|
|
297
|
+
if (!label || !control || !label.checkVisibility() || !control.checkVisibility()) continue
|
|
298
|
+
const a = label.getBoundingClientRect(), b = control.getBoundingClientRect()
|
|
299
|
+
out.push({ label: label.textContent.trim().slice(0, 20), sameLine: a.top < b.bottom && b.top < a.bottom, leftOf: a.right <= b.left + 1 })
|
|
300
|
+
}
|
|
301
|
+
const boxed = all.filter(e => e.matches?.('input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=color]), select') && e.checkVisibility() && !e.closest('div[cell]'))
|
|
302
|
+
.map(e => { const s = getComputedStyle(e); return { top: s.borderTopWidth, left: s.borderLeftWidth, bottom: s.borderBottomWidth } })
|
|
303
|
+
return { out, boxed }
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
/* The figure's own group is the modeller page's (figure-properties), not this page's: its rows are checked in screen.check. */
|
|
307
|
+
for (const part of ['bed', 'roller']) {
|
|
308
|
+
test(`${part}: every label beside its field, every field a line`, async () => {
|
|
309
|
+
await pick(part)
|
|
310
|
+
const { out, boxed } = await rows()
|
|
311
|
+
assert.ok(out.length >= 2, `rows read: ${out.length}`)
|
|
312
|
+
assert.deepEqual(out.filter(r => !r.sameLine || !r.leftOf), [], 'labels on their field\'s line, to its left')
|
|
313
|
+
assert.ok(boxed.length > 0, 'fields read')
|
|
314
|
+
assert.deepEqual(boxed.filter(b => b.top !== '0px' || b.left !== '0px' || b.bottom !== '1px'), [], 'only the line under it')
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
})
|
|
@@ -60,6 +60,10 @@ describe('a row\'s preview follows its basis', () => {
|
|
|
60
60
|
const at = await read(() => probe.row('bin'))
|
|
61
61
|
await page().mouse.click(at.x, at.y)
|
|
62
62
|
await until(() => probe.state().selected === 'bin', [], 'bin selected')
|
|
63
|
+
/* The row is the 「반응형」 tab's (user 2026-10-17). */
|
|
64
|
+
const tab = await read(() => probe.tab('figure.label.tab-short-response'))
|
|
65
|
+
await page().mouse.click(tab.x, tab.y)
|
|
66
|
+
await until(() => document.querySelector('figure-editor').shadowRoot.querySelector('button[tab][on]')?.getAttribute('aria-label') === 'figure.label.tab-short-response', [], 'the 반응형 tab on')
|
|
63
67
|
await click('details[row-fold] > summary', {}, 'the row\'s fold')
|
|
64
68
|
})
|
|
65
69
|
|
|
@@ -191,13 +191,13 @@ describe('the frame: parts · canvas · one side of seven tabs, and the bar over
|
|
|
191
191
|
assert.equal(await read(() => getComputedStyle(screenCheck.editor()).containerType), 'inline-size')
|
|
192
192
|
})
|
|
193
193
|
|
|
194
|
-
test('the side\'s
|
|
194
|
+
test('the side\'s six tabs, in order, each saying its name under its icon -- the name it is read by', async () => {
|
|
195
195
|
const tabs = await read(() =>
|
|
196
196
|
[...screenCheck.editor().shadowRoot.querySelectorAll('aside button[tab]')].map(b => ({ label: b.getAttribute('aria-label'), shows: b.querySelector('span[tab-label]')?.textContent.trim() }))
|
|
197
197
|
)
|
|
198
198
|
assert.deepEqual(
|
|
199
199
|
tabs.map(t => t.label),
|
|
200
|
-
['properties', 'motion', 'data', 'verdict'].map(TAB)
|
|
200
|
+
['properties', 'response', 'motion', 'capability', 'data', 'verdict'].map(TAB)
|
|
201
201
|
)
|
|
202
202
|
for (const tab of tabs) assert.equal(tab.shows, tab.label, `${tab.label} shows its name`)
|
|
203
203
|
assert.equal(await read(() => screenCheck.editor().shadowRoot.querySelector('aside').firstElementChild.matches('div[tabs]')), true, 'the tabs are the side\'s first row')
|
|
@@ -206,12 +206,12 @@ describe('the frame: parts · canvas · one side of seven tabs, and the bar over
|
|
|
206
206
|
test('a tab shows its panel and hides the others without taking them down -- the verdict keeps what it holds', async () => {
|
|
207
207
|
const panels = () =>
|
|
208
208
|
read(() => [...screenCheck.editor().shadowRoot.querySelectorAll('aside > div[panel]')].map(p => ({ seen: p.getBoundingClientRect().height > 0, connected: p.isConnected, verdict: !!p.querySelector('slot[name=verdict]') })))
|
|
209
|
-
const tabs = ['properties', 'motion', 'data', 'verdict']
|
|
209
|
+
const tabs = ['properties', 'response', 'motion', 'capability', 'data', 'verdict']
|
|
210
210
|
for (const [at, tab] of tabs.entries()) {
|
|
211
211
|
await pressTab(TAB(tab))
|
|
212
212
|
await until(`${tab} on`, name => screenCheck.editor().shadowRoot.querySelector('button[tab][on]')?.getAttribute('aria-label') === name, TAB(tab))
|
|
213
213
|
const now = await panels()
|
|
214
|
-
assert.equal(now.length,
|
|
214
|
+
assert.equal(now.length, 6, 'six panels, all standing')
|
|
215
215
|
assert.deepEqual(now.map(p => p.seen), tabs.map((_, i) => i === at), `${tab}: its panel seen, the rest hidden`)
|
|
216
216
|
assert.ok(now.every(p => p.connected))
|
|
217
217
|
assert.ok(now[tabs.indexOf('verdict')].verdict, 'the verdict panel holds the verdict slot whichever tab is on')
|
|
@@ -329,7 +329,7 @@ describe('the editor is drawn in the shell\'s tokens (2026-09-27)', () => {
|
|
|
329
329
|
describe('a part chosen: each tab in its place, the figure-wide tabs give way, the list\'s root goes back', () => {
|
|
330
330
|
before(() => mount('arm'))
|
|
331
331
|
|
|
332
|
-
test('properties hold the part\'s size
|
|
332
|
+
test('properties hold the part\'s size and place; 「반응형」 how it answers a size; capability its places; motion its motion and controls -- each under the part\'s head', async () => {
|
|
333
333
|
await pressRow('upper-arm')
|
|
334
334
|
await until('upper-arm chosen', () => screenCheck.editor().selected === 'upper-arm')
|
|
335
335
|
const holds = () =>
|
|
@@ -349,16 +349,17 @@ describe('a part chosen: each tab in its place, the figure-wide tabs give way, t
|
|
|
349
349
|
assert.ok(now.subject, 'the part\'s head')
|
|
350
350
|
assert.ok(now.links.includes('figure.label.group-size') && now.links.includes('figure.label.group-place'), `the shape's links: ${now.links}`)
|
|
351
351
|
assert.ok(now.material, 'its material')
|
|
352
|
-
/*
|
|
353
|
-
await
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
assert.ok(
|
|
361
|
-
assert.ok(
|
|
352
|
+
/* How it answers a size has a tab of its own (user 2026-10-17, 「반응형」). */
|
|
353
|
+
await pressTab(TAB('response'))
|
|
354
|
+
now = await holds()
|
|
355
|
+
assert.ok(now.subject, 'the part\'s head')
|
|
356
|
+
assert.ok(now.links.includes('figure.label.group-size'), `the response's links: ${now.links}`)
|
|
357
|
+
/* What it does is a tab again. */
|
|
358
|
+
await pressTab(TAB('capability'))
|
|
359
|
+
now = await holds()
|
|
360
|
+
assert.ok(now.subject, 'the part\'s head')
|
|
361
|
+
assert.ok(now.links.length === 1 && now.links[0].startsWith('figure.label.places-on-part'), `the places on the part: ${now.links}`)
|
|
362
|
+
assert.ok(now.capability, 'its capabilities')
|
|
362
363
|
await pressTab(TAB('motion'))
|
|
363
364
|
now = await holds()
|
|
364
365
|
assert.ok(now.subject, 'the part\'s head')
|
|
@@ -812,8 +813,11 @@ describe('a part\'s size row: a rule\'s numbers only where a rule sets every sid
|
|
|
812
813
|
await mount('box')
|
|
813
814
|
await pressRow('box')
|
|
814
815
|
await until('box chosen', () => screenCheck.editor().selected === 'box')
|
|
815
|
-
|
|
816
|
+
/* Following the figure's size is the 「반응형」 tab's; the size row it gives is the properties'. */
|
|
817
|
+
await pressTab(TAB('response'))
|
|
818
|
+
await press('follow the figure\'s size', () => screenCheck.deep('button[follow-size]').find(b => b.checkVisibility()))
|
|
816
819
|
await until('following', () => ['x', 'y', 'z'].every(axis => !screenCheck.editor().session.read().fields.some(f => f.id === `box.size.${axis}`)))
|
|
820
|
+
await pressTab(TAB('properties'))
|
|
817
821
|
assert.deepEqual(await sizeRow(), { held: 3 }, 'three sides, each held by the rule')
|
|
818
822
|
})
|
|
819
823
|
})
|
|
@@ -841,9 +845,12 @@ describe('the side panel is found as it was left, per part', () => {
|
|
|
841
845
|
test('scrolled and a fold opened on one part; another part opens at its top; back, the first is as it was left', async () => {
|
|
842
846
|
await pressRow('upper-arm')
|
|
843
847
|
await until('upper-arm chosen', () => screenCheck.editor().selected === 'upper-arm')
|
|
848
|
+
/* 「더 보기」 starts open on this page: folded first, so the fold opened below is one this part leaves open. */
|
|
849
|
+
if (await read(() => !!screenCheck.shownPanel().querySelector('section[more] > details[open]')))
|
|
850
|
+
await press('the more fold', () => screenCheck.shownPanel().querySelector('section[more] > details > summary'))
|
|
844
851
|
await press('a fold', () => {
|
|
845
852
|
const panel = screenCheck.shownPanel()
|
|
846
|
-
const closed = [...panel.querySelectorAll('figure-links')].flatMap(l => [...l.shadowRoot.querySelectorAll('details')]).find(d => !d.open)
|
|
853
|
+
const closed = [...panel.querySelectorAll('details'), ...[...panel.querySelectorAll('figure-links')].flatMap(l => [...l.shadowRoot.querySelectorAll('details')])].find(d => !d.open && d.checkVisibility())
|
|
847
854
|
return closed?.querySelector('summary')
|
|
848
855
|
})
|
|
849
856
|
const opened = await folds()
|
|
@@ -1008,6 +1015,28 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
|
|
|
1008
1015
|
}
|
|
1009
1016
|
})
|
|
1010
1017
|
|
|
1018
|
+
/* The figure's own group in the side's one-line rows (user 2026-10-17): the identifier, the name, where it stands. */
|
|
1019
|
+
test('the figure panel\'s labels stand beside their fields, the identifier\'s too, each field a line', async () => {
|
|
1020
|
+
await pressTab(TAB('properties'))
|
|
1021
|
+
const rows = await read(() => {
|
|
1022
|
+
const properties = screenCheck.deep('figure-properties')[0]
|
|
1023
|
+
const out = []
|
|
1024
|
+
const type = properties.querySelector('label[type-field]')
|
|
1025
|
+
const pairs = [[type?.querySelector('span'), type?.querySelector('input, code')]]
|
|
1026
|
+
for (const field of properties.shadowRoot.querySelectorAll('div[field]')) pairs.push([field.querySelector(':scope > label'), field.querySelector(':scope > input, :scope > select, :scope > div[row]')])
|
|
1027
|
+
for (const [label, control] of pairs) {
|
|
1028
|
+
if (!label || !control || !label.checkVisibility() || !control.checkVisibility()) continue
|
|
1029
|
+
const a = label.getBoundingClientRect(), b = control.getBoundingClientRect()
|
|
1030
|
+
const line = control.matches('input, select') ? getComputedStyle(control) : null
|
|
1031
|
+
out.push({ label: label.textContent.trim(), sameLine: a.top < b.bottom && b.top < a.bottom, leftOf: a.right <= b.left + 1, box: line ? line.borderTopWidth !== '0px' || line.borderLeftWidth !== '0px' : false })
|
|
1032
|
+
}
|
|
1033
|
+
return out
|
|
1034
|
+
})
|
|
1035
|
+
assert.ok(rows.length >= 3, `rows read: ${rows.map(r => r.label)}`)
|
|
1036
|
+
assert.deepEqual(rows.filter(r => !r.sameLine || !r.leftOf), [], 'labels on their field\'s line, to its left')
|
|
1037
|
+
assert.deepEqual(rows.filter(r => r.box), [], 'fields drawn as a line, not a box')
|
|
1038
|
+
})
|
|
1039
|
+
|
|
1011
1040
|
test('the figure\'s type is the first field of the figure panel, under its heading', async () => {
|
|
1012
1041
|
await pressTab(TAB('properties'))
|
|
1013
1042
|
const first = await read(() => {
|
|
@@ -1143,8 +1172,9 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
|
|
|
1143
1172
|
await open('fig-p', 'arm-clip')
|
|
1144
1173
|
await pressRow('upper-arm')
|
|
1145
1174
|
await until('upper-arm chosen', () => screenCheck.editor().selected === 'upper-arm')
|
|
1146
|
-
/* The part's panel fits the side now
|
|
1147
|
-
|
|
1175
|
+
/* The part's panel fits the side now: its 「더 보기」 open makes it long enough to scroll. */
|
|
1176
|
+
if (await read(() => !!screenCheck.shownPanel().querySelector('section[more] > details:not([open])')))
|
|
1177
|
+
await press('the more fold', () => screenCheck.shownPanel().querySelector('section[more] > details:not([open]) > summary'))
|
|
1148
1178
|
const at = await read(() => {
|
|
1149
1179
|
const r = screenCheck.shownPanel().getBoundingClientRect()
|
|
1150
1180
|
return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
|
|
@@ -24,7 +24,8 @@ import { buildPage } from './build.mjs'
|
|
|
24
24
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
25
25
|
const bundle = await buildPage('acceptance-page.js')
|
|
26
26
|
const dir = dirname(bundle)
|
|
27
|
-
|
|
27
|
+
/* The figure panel's words are figure-viewer's: both packages' Korean, as the app loads every module's. */
|
|
28
|
+
const ko = Buffer.from(JSON.stringify({ ...JSON.parse(readFileSync(resolve(HERE, '../../../figure-viewer/translations/ko.json'), 'utf-8')), ...JSON.parse(readFileSync(resolve(HERE, '../../translations/ko.json'), 'utf-8')) }))
|
|
28
29
|
const APP = resolve(process.env.OPERATO_APP ?? resolve(HERE, '../../../../../../operato-application/packages/operato-figure'))
|
|
29
30
|
/* The builder's ThemeOverridePlugin, run from the app root so its module order is the app's. */
|
|
30
31
|
const themeFiles = JSON.parse(
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Words that send the author somewhere name a place that is there (chief architect's ruling 2026-10-17: six side tabs,
|
|
3
|
+
* 「반응형」 holding size behaviour, the position that follows the size, the row and the range). The modeller once said
|
|
4
|
+
* 「움직임 탭의 치수에서」 and 「「위치와 크기」에서 「여기에 고정」」 after both had moved, and 「성숙도 탭」 for the tab named 검사.
|
|
5
|
+
*/
|
|
6
|
+
import { test } from 'node:test'
|
|
7
|
+
import assert from 'node:assert/strict'
|
|
8
|
+
import { readFileSync } from 'node:fs'
|
|
9
|
+
|
|
10
|
+
const table = (path: string) => JSON.parse(readFileSync(new URL(path, import.meta.url), 'utf8')) as Record<string, string>
|
|
11
|
+
const GONE: Record<string, RegExp> = {
|
|
12
|
+
ko: /성숙도 탭|「위치와 크기」|움직임 탭의 (치수|붙이기)/,
|
|
13
|
+
en: /maturity tab|under placement|under size and placement|(sizes|fastening) in the motion tab/
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test('no message sends the author to a tab or group the side no longer has', () => {
|
|
17
|
+
for (const lang of ['ko', 'en'] as const)
|
|
18
|
+
for (const path of ['../translations/', '../../figure-service/translations/']) {
|
|
19
|
+
const said = Object.entries(table(`${path}${lang}.json`)).filter(([, words]) => GONE[lang]!.test(words))
|
|
20
|
+
assert.deepEqual(said, [], `${path}${lang}.json`)
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('the places named are the tabs as they are called', () => {
|
|
25
|
+
const ko = table('../translations/ko.json')
|
|
26
|
+
const service = table('../../figure-service/translations/ko.json')
|
|
27
|
+
assert.ok(ko['figure.text.gizmo-size-follows']!.includes(`${ko['figure.label.tab-short-response']} 탭`))
|
|
28
|
+
assert.ok(ko['figure.text.preview-blocked-by-errors']!.includes(`${ko['figure.label.tab-short-verdict']} 탭`))
|
|
29
|
+
for (const key of ['figure.gate.parts-split-on-resize.how', 'figure.gate.sizing-not-uniform.how'])
|
|
30
|
+
assert.ok(service[key]!.includes(`${ko['figure.label.tab-short-response']} 탭의 「${ko['figure.label.group-position-response']}」`), key)
|
|
31
|
+
})
|