@things-factory/figure-ui 10.1.152 → 10.1.153
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/ai-proposal.ts +14 -13
- package/client/modeller/command-preview-controller.ts +210 -0
- package/client/modeller/editor-styles.ts +307 -0
- package/client/modeller/editor.ts +58 -619
- package/client/modeller/figure-first-save.ts +9 -1
- package/client/modeller/modeller-keys.ts +60 -0
- package/client/modeller/pane-fold.ts +61 -0
- package/client/modeller/panel-memory.ts +53 -0
- package/client/modeller/refusal-teller.ts +93 -0
- package/client/modeller/refusals.ts +18 -1
- package/dist-client/modeller/ai-proposal.d.ts +6 -5
- package/dist-client/modeller/ai-proposal.js +9 -9
- package/dist-client/modeller/ai-proposal.js.map +1 -1
- package/dist-client/modeller/clips.d.ts +1 -1
- package/dist-client/modeller/command-preview-controller.d.ts +75 -0
- package/dist-client/modeller/command-preview-controller.js +178 -0
- package/dist-client/modeller/command-preview-controller.js.map +1 -0
- package/dist-client/modeller/editor-styles.d.ts +2 -0
- package/dist-client/modeller/editor-styles.js +307 -0
- package/dist-client/modeller/editor-styles.js.map +1 -0
- package/dist-client/modeller/editor.d.ts +5 -50
- package/dist-client/modeller/editor.js +69 -641
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-first-save.d.ts +1 -0
- package/dist-client/modeller/figure-first-save.js +8 -1
- package/dist-client/modeller/figure-first-save.js.map +1 -1
- package/dist-client/modeller/modeller-keys.d.ts +22 -0
- package/dist-client/modeller/modeller-keys.js +43 -0
- package/dist-client/modeller/modeller-keys.js.map +1 -0
- package/dist-client/modeller/pane-fold.d.ts +15 -0
- package/dist-client/modeller/pane-fold.js +60 -0
- package/dist-client/modeller/pane-fold.js.map +1 -0
- package/dist-client/modeller/panel-memory.d.ts +15 -0
- package/dist-client/modeller/panel-memory.js +54 -0
- package/dist-client/modeller/panel-memory.js.map +1 -0
- package/dist-client/modeller/refusal-teller.d.ts +36 -0
- package/dist-client/modeller/refusal-teller.js +81 -0
- package/dist-client/modeller/refusal-teller.js.map +1 -0
- package/dist-client/modeller/refusals.d.ts +10 -0
- package/dist-client/modeller/refusals.js +9 -0
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/test/browser/fix-alignment.check.mjs +13 -2
- package/test/browser/links.check.mjs +36 -9
- package/test/browser/modeller.check.mjs +25 -0
- package/test/browser/parts-page.js +9 -1
- package/test/browser/parts.check.mjs +22 -0
- package/test/browser/places-mesh.check.mjs +25 -9
- package/test/browser/product-page.check.mjs +42 -2
- package/test/browser/screen.check.mjs +33 -0
- package/test/clip-ai-proposal.test.ts +4 -1
- package/test/clip-consent.test.ts +9 -2
- package/test/layout.test.ts +3 -1
- package/test/refusals.test.ts +18 -10
- package/test/theme.test.ts +5 -15
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.153",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"clean": "npm run clean:server && npm run clean:client"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@hatiolab/figure-model": "^0.6.
|
|
30
|
+
"@hatiolab/figure-model": "^0.6.3",
|
|
31
31
|
"@hatiolab/things-scene": "^10.1.83",
|
|
32
32
|
"@material/web": "^2.0.0",
|
|
33
33
|
"@operato/data-grist": "^10.0.0",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"@operato/styles": "^10.0.0",
|
|
43
43
|
"@operato/utils": "^10.0.0",
|
|
44
44
|
"@things-factory/ai-assistant": "^10.1.150",
|
|
45
|
-
"@things-factory/figure-service": "^10.1.
|
|
45
|
+
"@things-factory/figure-service": "^10.1.153",
|
|
46
46
|
"three": "^0.185.1"
|
|
47
47
|
},
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "54b3e985a55d5d0b01622d87f385f07518fb5d16"
|
|
49
49
|
}
|
|
@@ -35,10 +35,21 @@ async function until(fn, args, what, ms = 8000) {
|
|
|
35
35
|
await sleep(50)
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
/* The editor and the part's panels have drawn what they were last told: a read never meets the draw before. */
|
|
39
|
+
const settled = () =>
|
|
40
|
+
read(async () => {
|
|
41
|
+
const deep = (s, root = document) => [...root.querySelectorAll(s)].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(s, e.shadowRoot)))
|
|
42
|
+
const drawing = () => [document.querySelector('figure-editor'), ...deep('figure-links')].filter(Boolean)
|
|
43
|
+
for (let round = 0; round < 20; round++) {
|
|
44
|
+
await Promise.all(drawing().map(e => e.updateComplete))
|
|
45
|
+
if (!drawing().some(e => e.isUpdatePending)) return
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
/** Pressed once it is on the page and the panels have drawn -- waited for, not looked up once. */
|
|
39
49
|
async function click(selector, options = {}, what = selector) {
|
|
50
|
+
await settled()
|
|
51
|
+
await until((s, o) => !!findDeep(s, o), [selector, options], `${what} on the page`)
|
|
40
52
|
const element = await page().evaluateHandle((s, o) => findDeep(s, o), selector, options)
|
|
41
|
-
assert.ok(await element.evaluate(e => !!e), `${what} is on the page`)
|
|
42
53
|
await element.evaluate(e => e.scrollIntoView({ block: 'center' }))
|
|
43
54
|
await element.click()
|
|
44
55
|
await settled()
|
|
@@ -101,6 +101,28 @@ const handle = (selector, test) =>
|
|
|
101
101
|
selector,
|
|
102
102
|
test
|
|
103
103
|
)
|
|
104
|
+
/**
|
|
105
|
+
* Pressed as the panels stand once drawn: no fastening panel with an update still to draw, then the element itself, not a
|
|
106
|
+
* point read before. A point read too early is pressed after the panel has moved under it (2026-10-11: the door leaf's
|
|
107
|
+
* "fasten" read, then its scroll put back a frame later, and the click fell on another button).
|
|
108
|
+
*/
|
|
109
|
+
async function press(selector, text, what = text ?? selector) {
|
|
110
|
+
await read(async () => {
|
|
111
|
+
const deep = (s, root = document) => [...root.querySelectorAll(s)].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(s, e.shadowRoot)))
|
|
112
|
+
for (let round = 0; round < 20 && deep('figure-links').some(e => e.isUpdatePending); round++) await Promise.all(deep('figure-links').map(e => e.updateComplete))
|
|
113
|
+
})
|
|
114
|
+
const element = await page().evaluateHandle(
|
|
115
|
+
(selector, text) => {
|
|
116
|
+
const deep = (s, root = document) => [...root.querySelectorAll(s)].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(s, e.shadowRoot)))
|
|
117
|
+
return deep(selector).find(e => text === undefined || e.textContent.replace(/\s+/g, ' ').trim().includes(text)) ?? null
|
|
118
|
+
},
|
|
119
|
+
selector,
|
|
120
|
+
text
|
|
121
|
+
)
|
|
122
|
+
assert.ok(await element.evaluate(e => !!e), `${what} is on the page`)
|
|
123
|
+
await element.click()
|
|
124
|
+
await settled()
|
|
125
|
+
}
|
|
104
126
|
/** Chooses an option of the select labelled `label`, as a person does: the select changes, once. */
|
|
105
127
|
async function choose(label, value, selector = 'select') {
|
|
106
128
|
const select = await handle(selector, label)
|
|
@@ -322,7 +344,7 @@ describe('a new fastening for a door leaf: seen before it is done', () => {
|
|
|
322
344
|
|
|
323
345
|
test('started, it asks what to fasten to, with nothing chosen', async () => {
|
|
324
346
|
await pick('leaf')
|
|
325
|
-
await
|
|
347
|
+
await press('button[act]', 'figure.button.fasten-start')
|
|
326
348
|
/* The form is the panel's, drawn after the editor's update. */
|
|
327
349
|
const target = await until(() => look.select('figure.label.target-part'), [], 'the fastening form')
|
|
328
350
|
assert.deepEqual([target.value, target.options[0].text], ['', 'figure.label.target-part-placeholder'])
|
|
@@ -368,7 +390,7 @@ describe('a new fastening for a door leaf: seen before it is done', () => {
|
|
|
368
390
|
|
|
369
391
|
test('"fasten it first" from the motion tab opens this part\'s fastening, as it is now, in view, on the properties tab', async () => {
|
|
370
392
|
await pick('leaf')
|
|
371
|
-
await
|
|
393
|
+
await press('button[act]', 'figure.button.fasten-start')
|
|
372
394
|
await choose('figure.label.target-part', 'frame')
|
|
373
395
|
/* The panel scrolled back to its head, the fastening below the fold. */
|
|
374
396
|
const panel = await at('section[fastening]')
|
|
@@ -406,6 +428,14 @@ describe('a new fastening for a door leaf: seen before it is done', () => {
|
|
|
406
428
|
})
|
|
407
429
|
})
|
|
408
430
|
|
|
431
|
+
/*
|
|
432
|
+
* 정렬·간격 고치기 pressed as the panel stands once it has drawn: a point read before it has, then clicked, can land where
|
|
433
|
+
* the button was, and nothing happens.
|
|
434
|
+
*/
|
|
435
|
+
async function pressFixAlignment() {
|
|
436
|
+
await press('div[overhang] button[fix-alignment]', undefined, 'fix alignment')
|
|
437
|
+
}
|
|
438
|
+
|
|
409
439
|
describe('past the edge of the face a part is fastened to', () => {
|
|
410
440
|
before(async () => {
|
|
411
441
|
let a = fasten(box(createFigureAsset('Overhangs'), 'frame', [1000, 100, 400], [0, 50, 0]), 'frame', 'plane')
|
|
@@ -422,7 +452,7 @@ describe('past the edge of the face a part is fastened to', () => {
|
|
|
422
452
|
await pick('out')
|
|
423
453
|
await until(() => look.all('div[overhang] p', 'figure.text.overhang-out').length === 1, [], 'said past the edge')
|
|
424
454
|
const start = await state()
|
|
425
|
-
await
|
|
455
|
+
await pressFixAlignment()
|
|
426
456
|
await until(r => probe.state().revision === r + 1, [start.revision], 'one step')
|
|
427
457
|
await until(() => look.all('div[overhang] p', 'figure.text.overhang-out').length === 0, [], 'no longer past the edge')
|
|
428
458
|
})
|
|
@@ -436,7 +466,7 @@ describe('past the edge of the face a part is fastened to', () => {
|
|
|
436
466
|
await pick('cap')
|
|
437
467
|
await until(() => look.all('div[overhang] p', 'figure.text.overhang-unknown').length === 1, [], 'said not known')
|
|
438
468
|
const start = await state()
|
|
439
|
-
await
|
|
469
|
+
await pressFixAlignment()
|
|
440
470
|
await until(() => look.all('p[fix-note]', 'figure.text.fix-alignment-unmeasured').length === 1, [], 'why it cannot')
|
|
441
471
|
assert.equal((await state()).revision, start.revision, 'nothing written')
|
|
442
472
|
})
|
|
@@ -489,11 +519,8 @@ describe('a bench: what each part is fastened to, what detaching would do, a row
|
|
|
489
519
|
await pick('lamp')
|
|
490
520
|
assert.deepEqual((await all('details[row-fold]')).map(d => d.open), [false], 'folded')
|
|
491
521
|
await read(() => look.hear('preview-command'))
|
|
492
|
-
/*
|
|
493
|
-
|
|
494
|
-
toggle then closes it again -- two clicks open it (seen 2026-10-11).
|
|
495
|
-
*/
|
|
496
|
-
for (let i = 0; i < 2 && !(await all('details[row-fold]'))[0].open; i++) await click(await at('details[row-fold] > summary'), 'the row')
|
|
522
|
+
/* One click opens it, and it stays open while the row is worked. */
|
|
523
|
+
await click(await at('details[row-fold] > summary'), 'the row')
|
|
497
524
|
assert.deepEqual((await all('details[row-fold]')).map(d => d.open), [true], 'open while it is worked')
|
|
498
525
|
await until(() => look.all('section[row] input[type=number]').length > 0, [], 'the pitch field in the open row')
|
|
499
526
|
await type(await handle('section[row] input[type=number]'), '250')
|
|
@@ -900,6 +900,31 @@ describe('the row section opens on one click', () => {
|
|
|
900
900
|
await click(at)
|
|
901
901
|
assert.equal(await summary.evaluate(s => s.parentElement.open), false, 'folded after a second')
|
|
902
902
|
})
|
|
903
|
+
|
|
904
|
+
test('a part picked again: a fold opened at once stays open -- the folds kept for the part are put back first', async () => {
|
|
905
|
+
await mount('rollers')
|
|
906
|
+
await click(await read(() => probe.row('mark')))
|
|
907
|
+
await click(await read(() => probe.row('roller')))
|
|
908
|
+
const open = await read(async () => {
|
|
909
|
+
const deep = (selector, root = document) => [...root.querySelectorAll(selector)].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(selector, e.shadowRoot)))
|
|
910
|
+
const editor = deep('figure-editor')[0]
|
|
911
|
+
/*
|
|
912
|
+
The panel is drawn for the part once the editor and its links have updated; a click can come any time after
|
|
913
|
+
that, before a frame is drawn. Here it comes right then.
|
|
914
|
+
*/
|
|
915
|
+
editor.selected = 'mark'
|
|
916
|
+
await editor.updateComplete
|
|
917
|
+
await Promise.all(deep('figure-links').map(links => links.updateComplete))
|
|
918
|
+
await null
|
|
919
|
+
const summary = deep('details[row-fold] > summary')[0]
|
|
920
|
+
summary.click()
|
|
921
|
+
const fold = summary.parentElement
|
|
922
|
+
const opened = fold.open
|
|
923
|
+
await new Promise(done => requestAnimationFrame(() => requestAnimationFrame(done)))
|
|
924
|
+
return [opened, fold.open]
|
|
925
|
+
})
|
|
926
|
+
assert.deepEqual(open, [true, true], 'opened by the click, and still open a frame later')
|
|
927
|
+
})
|
|
903
928
|
})
|
|
904
929
|
|
|
905
930
|
test('the page raised no error', () => {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Only reading: every change the check makes goes through the mouse and the keyboard.
|
|
6
6
|
*/
|
|
7
7
|
import './editing-page.js'
|
|
8
|
-
import { addFigureBox, applyFigureAuthoring, createFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
8
|
+
import { addFigureBox, applyFigureAuthoring, createFigureAsset, proposeFigureOccupancy, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
9
9
|
|
|
10
10
|
const PALETTE = {
|
|
11
11
|
'palette.dark': '#2b2b2b',
|
|
@@ -36,6 +36,14 @@ Object.assign(window.figures, {
|
|
|
36
36
|
return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'attach', part: 'leaf', face: 'bottom', to: { part: 'frame', face: 'top' } }))
|
|
37
37
|
},
|
|
38
38
|
/* A box with rounded corners: made from the box, still called what it is. */
|
|
39
|
+
/* A housing and a stray part on the floor, the space the figure takes declared from both. */
|
|
40
|
+
gate: () => {
|
|
41
|
+
let a = figure('Gate')
|
|
42
|
+
a = addFigureBox(a, { id: 'housing', size: { x: 300, y: 1000, z: 300 }, position: { x: 0, y: 500, z: 0 } })
|
|
43
|
+
a = addFigureBox(a, { id: 'stray', size: { x: 100, y: 100, z: 100 }, position: { x: 600, y: 50, z: 0 } })
|
|
44
|
+
for (const part of ['housing', 'stray']) a = applyFigureAuthoring(a, { kind: 'attach', part, face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
45
|
+
return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a), placement: 'floor' }))
|
|
46
|
+
},
|
|
39
47
|
rounded: () => {
|
|
40
48
|
let a = figure('Rounded')
|
|
41
49
|
a = addFigureBox(a, { id: 'tray', size: { x: 600, y: 200, z: 400 }, position: { x: 0, y: 100, z: 0 } })
|
|
@@ -239,6 +239,28 @@ describe('a part renamed in its name field: one step, still selected; a name ref
|
|
|
239
239
|
})
|
|
240
240
|
})
|
|
241
241
|
|
|
242
|
+
describe('a part in the declared space removed: at once, the space fitted to the parts left, and said so (2026-09-27)', () => {
|
|
243
|
+
test('one step, nothing asked, the space still declared, and one notice that it was fitted again', async () => {
|
|
244
|
+
await mount('box', 'gate')
|
|
245
|
+
await pick('stray')
|
|
246
|
+
const before = await revision()
|
|
247
|
+
const space = () => read(() => JSON.parse(document.querySelector('figure-editor').session.source()).occupancy ?? null)
|
|
248
|
+
const was = await space()
|
|
249
|
+
assert.ok(was, 'declared to begin with')
|
|
250
|
+
await click(await read(() => parts.op('remove-part')), 'remove')
|
|
251
|
+
assert.deepEqual(await ids(), ['housing'])
|
|
252
|
+
assert.equal(await revision(), before + 1, 'one step')
|
|
253
|
+
assert.equal(await read(() => editing.prompt()), null, 'nothing asked')
|
|
254
|
+
const now = await space()
|
|
255
|
+
assert.ok(now, 'the space is still declared')
|
|
256
|
+
assert.notDeepEqual(now, was, 'fitted to the part left')
|
|
257
|
+
const told = await read(() => editing.told())
|
|
258
|
+
assert.deepEqual(told.map(t => t.level), ['info'], JSON.stringify(told))
|
|
259
|
+
/* The page loads no translations: the words are the key. */
|
|
260
|
+
assert.ok(told[0].message.includes('figure.text.removal-space-refitted'), told[0].message)
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
242
264
|
describe('the list narrowed: the selected row keeps its name and its buttons in sight', () => {
|
|
243
265
|
before(() => mount('box', 'three'))
|
|
244
266
|
after(() => read(() => parts.narrow(0)))
|
|
@@ -42,24 +42,38 @@ async function until(fn, args, what, ms = 8000) {
|
|
|
42
42
|
await sleep(50)
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
/*
|
|
46
|
+
* Every element the check reads has drawn what it was last told: the editor, and the places panel and the shared slot
|
|
47
|
+
* layout editor inside it, which draw after the editor does -- awaited until none of them has a draw pending, so a read
|
|
48
|
+
* never meets the one before.
|
|
49
|
+
*/
|
|
50
|
+
const settled = () =>
|
|
51
|
+
read(async () => {
|
|
52
|
+
const deep = (s, root = document) => [...root.querySelectorAll(s)].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(s, e.shadowRoot)))
|
|
53
|
+
const drawing = () => [document.querySelector('figure-editor'), ...deep('figure-links, figure-slot-layout-panel, ox-slot-layout-editor')].filter(Boolean)
|
|
54
|
+
for (let round = 0; round < 20; round++) {
|
|
55
|
+
await Promise.all(drawing().map(e => e.updateComplete))
|
|
56
|
+
if (!drawing().some(e => e.isUpdatePending)) return
|
|
57
|
+
}
|
|
58
|
+
})
|
|
46
59
|
|
|
47
60
|
async function mount(asset) {
|
|
48
61
|
await read(text => harness.mount('given', text), serializeFigureAsset(asset))
|
|
49
62
|
await until(() => look.drawn().length > 0, [], 'the board is drawn')
|
|
50
63
|
}
|
|
51
|
-
/** The element `findDeep` finds,
|
|
52
|
-
|
|
64
|
+
/** The element `findDeep` finds, once it is on the page -- waited for, not looked up once -- as a handle to act on. */
|
|
65
|
+
async function handle(selector, options = {}, what = selector) {
|
|
66
|
+
await until((s, o) => !!findDeep(s, o), [selector, options], `${what} on the page`)
|
|
67
|
+
return page().evaluateHandle((s, o) => findDeep(s, o), selector, options)
|
|
68
|
+
}
|
|
53
69
|
async function click(selector, options = {}, what = selector) {
|
|
54
|
-
const element = await handle(selector, options)
|
|
55
|
-
assert.ok(await element.evaluate(e => !!e), `${what} is on the page`)
|
|
70
|
+
const element = await handle(selector, options, what)
|
|
56
71
|
await element.evaluate(e => e.scrollIntoView({ block: 'center' }))
|
|
57
72
|
await element.click()
|
|
58
73
|
await settled()
|
|
59
74
|
}
|
|
60
75
|
async function type(selector, options, text) {
|
|
61
76
|
const field = await handle(selector, options)
|
|
62
|
-
assert.ok(await field.evaluate(e => !!e), `${selector} is on the page`)
|
|
63
77
|
await field.evaluate(e => e.scrollIntoView({ block: 'center' }))
|
|
64
78
|
await field.click({ clickCount: 3 })
|
|
65
79
|
await page().keyboard.type(text)
|
|
@@ -68,7 +82,6 @@ async function type(selector, options, text) {
|
|
|
68
82
|
}
|
|
69
83
|
async function choose(selector, options, value) {
|
|
70
84
|
const select = await handle(selector, options)
|
|
71
|
-
assert.ok(await select.evaluate(e => !!e), `${selector} is on the page`)
|
|
72
85
|
await select.select(value)
|
|
73
86
|
await settled()
|
|
74
87
|
}
|
|
@@ -120,7 +133,8 @@ describe('a GLB made into a mesh part', () => {
|
|
|
120
133
|
})
|
|
121
134
|
|
|
122
135
|
test('the file chooser takes GLB files', async () => {
|
|
123
|
-
|
|
136
|
+
const input = await handle('input[mesh-file]', {}, 'the palette\'s file input')
|
|
137
|
+
assert.equal(await input.evaluate(i => i.accept), '.glb,model/gltf-binary')
|
|
124
138
|
})
|
|
125
139
|
|
|
126
140
|
test('a file with one node: uploaded, added at once as one step, named by the file, selected', async () => {
|
|
@@ -142,7 +156,7 @@ describe('a GLB made into a mesh part', () => {
|
|
|
142
156
|
await read(a => meshServer.answer({ upload: a }), crane)
|
|
143
157
|
const start = await state()
|
|
144
158
|
await importFile()
|
|
145
|
-
await until(() =>
|
|
159
|
+
await until(() => (findDeep('figure-mesh-choice')?.shadowRoot?.querySelectorAll('li button').length ?? 0) === 3, [], 'the question and its three choices')
|
|
146
160
|
const choices = await read(() => [...findDeep('figure-mesh-choice').shadowRoot.querySelectorAll('li button')].map(b => b.textContent.replace(/\s+/g, ' ').trim()))
|
|
147
161
|
assert.equal(choices.length, 3)
|
|
148
162
|
assert.match(choices[0], /^figure\.text\.mesh-whole-file/)
|
|
@@ -186,6 +200,7 @@ describe('places made by a rule: the shared slot layout editor on a part\'s face
|
|
|
186
200
|
await pick('fork')
|
|
187
201
|
const tab = await read(l => probe.tab(l), 'figure.label.tab-short-capability')
|
|
188
202
|
await page().mouse.click(tab.x, tab.y)
|
|
203
|
+
await until(() => look.tab() === 'figure.label.tab-short-capability', [], 'the capability tab on')
|
|
189
204
|
await settled()
|
|
190
205
|
})
|
|
191
206
|
|
|
@@ -193,6 +208,7 @@ describe('places made by a rule: the shared slot layout editor on a part\'s face
|
|
|
193
208
|
assert.equal(await has('figure-slot-layout-panel'), false)
|
|
194
209
|
await click('md-filter-chip[data-value="rule"]', {}, 'the by-a-rule choice')
|
|
195
210
|
await until(() => !!findDeep('ox-slot-layout-editor'), [], 'the editor')
|
|
211
|
+
await settled()
|
|
196
212
|
assert.equal(await has('button[data-action="use-default"]'), false, 'no default: the figure\'s layout is the only one')
|
|
197
213
|
})
|
|
198
214
|
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import { test, describe, before, after } from 'node:test'
|
|
11
11
|
import assert from 'node:assert/strict'
|
|
12
12
|
|
|
13
|
+
import { addFigureBox, applyFigureAuthoring, createFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
14
|
+
|
|
13
15
|
import { buildPage } from './build.mjs'
|
|
14
16
|
import { openPage } from './serve.mjs'
|
|
15
17
|
|
|
@@ -203,7 +205,9 @@ describe('the first save of a new figure', () => {
|
|
|
203
205
|
await until(() => !product.deep('figure-first-save').length, 'the question closed')
|
|
204
206
|
await press('button[save]', 'save')
|
|
205
207
|
await until(() => product.deep('figure-first-save')[0]?.shadowRoot.querySelector('#type'), 'asked again')
|
|
206
|
-
|
|
208
|
+
/* Asked again, the name is focused again: the browser honours `autofocus` once per document. */
|
|
209
|
+
await until(() => { let a = document.activeElement; while (a?.shadowRoot?.activeElement) a = a.shadowRoot.activeElement; return a?.id === 'name' }, 'the name focused again')
|
|
210
|
+
assert.deepEqual(await firstSave(), { first: 'name', type: 'my-lift', focused: 'name' })
|
|
207
211
|
await read(() => {
|
|
208
212
|
const name = product.deep('figure-first-save >>> #name')[0]
|
|
209
213
|
name.focus()
|
|
@@ -566,6 +570,42 @@ describe('the page around the figure: the shell, the dock, the properties', () =
|
|
|
566
570
|
})
|
|
567
571
|
})
|
|
568
572
|
|
|
573
|
+
describe('a placement the figure\'s parts refuse, chosen on the properties tab (wall placement, 09-25)', () => {
|
|
574
|
+
/* A post standing on the floor's plane by its bottom: a wall takes the back face, so the wall is refused. */
|
|
575
|
+
const post = () => {
|
|
576
|
+
const a = addFigureBox(createFigureAsset('Post'), { id: 'post', size: { x: 100, y: 1000, z: 100 }, position: { x: 0, y: 500, z: 0 } })
|
|
577
|
+
return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'attach', part: 'post', face: 'bottom', to: { plane: 'mounting-plane' } }))
|
|
578
|
+
}
|
|
579
|
+
const placement = () =>
|
|
580
|
+
read(() => {
|
|
581
|
+
const select = product.deep('figure-modeller-page >>> figure-properties >>> select').find(s => s.previousElementSibling?.textContent.trim() === 'figure.label.figure-placement')
|
|
582
|
+
return select ? select.value : null
|
|
583
|
+
})
|
|
584
|
+
const revision = () => read(() => product.page().session.read().revision)
|
|
585
|
+
|
|
586
|
+
before(async () => {
|
|
587
|
+
await fresh()
|
|
588
|
+
await read(text => product.seed({ id: 'p', type: 'post', source: text }), post())
|
|
589
|
+
await open('p')
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
test('refused: nothing written, the reason said in words beside the tabs, and the list back on the placement it has', async () => {
|
|
593
|
+
assert.equal(await placement(), 'floor')
|
|
594
|
+
const start = await revision()
|
|
595
|
+
await read(() => {
|
|
596
|
+
const select = product.deep('figure-modeller-page >>> figure-properties >>> select').find(s => s.previousElementSibling?.textContent.trim() === 'figure.label.figure-placement')
|
|
597
|
+
select.value = 'wall'
|
|
598
|
+
select.dispatchEvent(new Event('change', { bubbles: true }))
|
|
599
|
+
})
|
|
600
|
+
const said = await until(() => product.deep('figure-modeller-page >>> p[fit-error]')[0]?.textContent.trim(), 'the refusal said')
|
|
601
|
+
/* Said from the kernel's details (ADR-0127): the page loads no translations, so the words are the key with its values. */
|
|
602
|
+
assert.match(said, /^figure\.text\./, said)
|
|
603
|
+
assert.ok(said.includes('post') && said.includes('back'), `names the part and the face the wall takes: ${said}`)
|
|
604
|
+
assert.equal(await revision(), start, 'nothing written')
|
|
605
|
+
assert.equal(await placement(), 'floor', 'the list says the placement the figure has')
|
|
606
|
+
})
|
|
607
|
+
})
|
|
608
|
+
|
|
569
609
|
describe('AI proposals reaching the page', () => {
|
|
570
610
|
before(async () => {
|
|
571
611
|
await fresh()
|
|
@@ -593,7 +633,7 @@ describe('AI proposals reaching the page', () => {
|
|
|
593
633
|
test('a proposal that would stop a clip is refused whole, the clip named', async () => {
|
|
594
634
|
const before = await read(() => product.view())
|
|
595
635
|
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'] })
|
|
636
|
+
assert.deepEqual({ applied: refused.applied, code: refused.code, clips: refused.clips?.map(c => c.clip) }, { applied: false, code: 'CLIPS_DROPPED', clips: ['swing'] })
|
|
597
637
|
const after = await read(() => product.view())
|
|
598
638
|
assert.deepEqual({ source: after.source, revision: after.revision }, { source: before.source, revision: before.revision })
|
|
599
639
|
})
|
|
@@ -287,6 +287,39 @@ describe('the frame: parts · canvas · one side of six tabs, and the bar over t
|
|
|
287
287
|
})
|
|
288
288
|
})
|
|
289
289
|
|
|
290
|
+
describe('the editor is drawn in the shell\'s tokens (2026-09-27)', () => {
|
|
291
|
+
/* Set apart from any theme, so a colour read back can only have come from the token named. */
|
|
292
|
+
const TOKENS = { '--md-sys-color-secondary-container': 'rgb(1, 2, 3)', '--md-sys-color-on-secondary-container': 'rgb(4, 5, 6)', '--md-sys-color-primary': 'rgb(7, 8, 9)' }
|
|
293
|
+
before(async () => {
|
|
294
|
+
await mount('box')
|
|
295
|
+
await read(tokens => Object.entries(tokens).forEach(([name, value]) => screenCheck.editor().style.setProperty(name, value)), TOKENS)
|
|
296
|
+
})
|
|
297
|
+
after(() => read(tokens => Object.keys(tokens).forEach(name => screenCheck.editor().style.removeProperty(name)), TOKENS))
|
|
298
|
+
|
|
299
|
+
test('a pressed button is drawn in the container the shell matched for both themes', async () => {
|
|
300
|
+
await pressRow('box')
|
|
301
|
+
await press('the move tool', () => screenCheck.editor().shadowRoot.querySelector('button[tool][aria-label="figure.button.gizmo-translate"]'))
|
|
302
|
+
await until('the move tool pressed', () => screenCheck.editor().shadowRoot.querySelector('button[tool][aria-pressed="true"]'))
|
|
303
|
+
/* Off the button: under the pointer it is drawn as hovered. */
|
|
304
|
+
await page().mouse.move(0, 0)
|
|
305
|
+
const drawn = await read(() => {
|
|
306
|
+
const root = screenCheck.editor().shadowRoot
|
|
307
|
+
const tool = getComputedStyle(root.querySelector('button[tool][aria-pressed="true"]'))
|
|
308
|
+
const mode = getComputedStyle(root.querySelector('button[mode][aria-pressed="true"]'))
|
|
309
|
+
return { tool: { background: tool.backgroundColor, color: tool.color }, mode: { color: mode.color, border: mode.borderTopColor } }
|
|
310
|
+
})
|
|
311
|
+
assert.deepEqual(drawn, { tool: { background: 'rgb(1, 2, 3)', color: 'rgb(4, 5, 6)' }, mode: { color: 'rgb(4, 5, 6)', border: 'rgb(7, 8, 9)' } })
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
test('the side draws no edge of its own and stacks over nothing: the frame draws the line between panes', async () => {
|
|
315
|
+
const aside = await read(() => {
|
|
316
|
+
const style = getComputedStyle(screenCheck.editor().shadowRoot.querySelector('aside'))
|
|
317
|
+
return { zIndex: style.zIndex, borderLeft: style.borderLeftWidth }
|
|
318
|
+
})
|
|
319
|
+
assert.deepEqual(aside, { zIndex: 'auto', borderLeft: '0px' })
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
290
323
|
describe('a part chosen: each tab in its place, the figure-wide tabs give way, the list\'s root goes back', () => {
|
|
291
324
|
before(() => mount('arm'))
|
|
292
325
|
|
|
@@ -10,6 +10,7 @@ import { addFigureBox, applyFigureAuthoring, createFigureAsset, editFigurePart,
|
|
|
10
10
|
|
|
11
11
|
import { createFigureProductSession } from '../client/modeller/product-session.js'
|
|
12
12
|
import { runFigureProposal, takeFigureOffer, type FigureApplyDetail } from '../client/modeller/ai-proposal.js'
|
|
13
|
+
import { clipLossOf } from '../client/modeller/refusals.js'
|
|
13
14
|
|
|
14
15
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
15
16
|
const ko = JSON.parse(read('../translations/ko.json'))
|
|
@@ -45,7 +46,7 @@ test('a proposal that would stop a clip is refused whole: nothing written, nothi
|
|
|
45
46
|
const out = await runFigureProposal({ steps, context: context!, revision: revision! }, pageOf(s), say)
|
|
46
47
|
assert.equal(out.verdict, 'refused')
|
|
47
48
|
assert.equal(out.errorCode, 'CLIPS_DROPPED')
|
|
48
|
-
assert.
|
|
49
|
+
assert.equal(out.note, say('figure.text.proposal-would-drop-clips', { clips: 'swing' }), 'said as the clips it would stop, not the kernel\'s English')
|
|
49
50
|
assert.equal(s.source(), before, 'not even the rename before it: refused whole')
|
|
50
51
|
assert.equal(s.read().revision, revision)
|
|
51
52
|
assert.equal(s.read().dirty, false, 'nothing to save')
|
|
@@ -93,6 +94,8 @@ test('an AI proposal made while a person\'s consent is held is refused; clips, f
|
|
|
93
94
|
s.withConsent({ dropClips: ['swing/open'], context: context!, revision: revision! }, () => takeFigureOffer(s, detail, { onSavedFigure: false, say }))
|
|
94
95
|
assert.equal(detail.applied, false)
|
|
95
96
|
assert.equal(detail.code, 'CLIPS_DROPPED')
|
|
97
|
+
/* Recorded as figure-model refused it, and read where every refusal is read. */
|
|
98
|
+
assert.deepEqual(clipLossOf(detail)?.map(c => `${c.clip} ${c.why}`), ['swing gone'])
|
|
96
99
|
assert.equal(s.source(), before)
|
|
97
100
|
assert.equal(s.read().revision, revision)
|
|
98
101
|
assert.equal(s.read().dirty, false)
|
|
@@ -10,6 +10,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
10
10
|
import { addFigureBox, applyFigureAuthoring, createFigureAsset, editFigurePart, parseFigureAsset, serializeFigureAsset, type FigureAsset } from '@hatiolab/figure-model'
|
|
11
11
|
|
|
12
12
|
import { createFigureProductSession } from '../client/modeller/product-session.js'
|
|
13
|
+
import { clipLossOf } from '../client/modeller/refusals.js'
|
|
13
14
|
|
|
14
15
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
15
16
|
|
|
@@ -37,9 +38,15 @@ test('refused and named first; agreed, exactly those go in one step; undo brings
|
|
|
37
38
|
const s = createFigureProductSession(store)
|
|
38
39
|
await s.open('doors')
|
|
39
40
|
const revision = s.read().revision
|
|
40
|
-
assert.throws(() => s.command(fixed(s, 'left')), (e: { code?: string; clips?: Array<{ id: string; clip: string; why: string }> }) =>
|
|
41
|
-
e.code === 'CLIPS_DROPPED' && e.clips?.map(c => `${c.clip} ${c.why}`).join() === 'left-swing gone')
|
|
41
|
+
assert.throws(() => s.command(fixed(s, 'left')), (e: { code?: string; details?: { reason?: string; clips?: Array<{ id: string; clip: string; why: string }> } }) =>
|
|
42
|
+
e.code === 'CLIPS_DROPPED' && e.details?.reason === 'clips-dropped' && e.details.clips?.map(c => `${c.clip} ${c.why}`).join() === 'left-swing gone')
|
|
42
43
|
assert.equal(s.read().revision, revision, 'nothing written')
|
|
44
|
+
/* The screen reads the refusal in one place: the clips to ask about, and nothing for any other refusal. */
|
|
45
|
+
const refusalOf = (run: () => unknown) => { try { run() } catch (error) { return error } }
|
|
46
|
+
assert.deepEqual(clipLossOf(refusalOf(() => s.command(fixed(s, 'left'))))?.map(c => `${c.clip} ${c.why}`), ['left-swing gone'])
|
|
47
|
+
const other = refusalOf(() => s.edit({ kind: 'remove-part', target: 'nowhere' }))
|
|
48
|
+
assert.ok((other as { code?: string })?.code, 'another refusal')
|
|
49
|
+
assert.equal(clipLossOf(other), null)
|
|
43
50
|
assert.deepEqual(clipsOf(s), ['left-swing', 'right-swing'])
|
|
44
51
|
const ids = ['left-swing/left-open']
|
|
45
52
|
const at = () => ({ context: s.read().context!, revision: s.read().revision! })
|
package/test/layout.test.ts
CHANGED
|
@@ -16,7 +16,9 @@ const renderBody = editor.slice(editor.indexOf(' render() {'), editor.indexOf('
|
|
|
16
16
|
test('띠 — 칸 접기 · 되돌리기 · 전체 보기 · 저장 상태 · 저장, 말은 번역에서', () => {
|
|
17
17
|
/* The bar itself -- its folds, undo · redo · fit, the status and save -- is the browser check's (test/browser/screen.check.mjs). */
|
|
18
18
|
assert.doesNotMatch(renderBody, /[가-힣]/, '편집기의 화면 말에 한국어가 박혀 있지 않다')
|
|
19
|
-
|
|
19
|
+
/* The editor's words, and those of the parts it hands its work to. */
|
|
20
|
+
const said = [editor, ...['pane-fold', 'refusal-teller', 'command-preview-controller'].map(name => read(`../client/modeller/${name}.ts`))].join('\n')
|
|
21
|
+
const keys = [...said.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1])
|
|
20
22
|
for (const lang of ['ko', 'en', 'ja', 'zh', 'ms', 'es']) {
|
|
21
23
|
const table = JSON.parse(read(`../translations/${lang}.json`))
|
|
22
24
|
for (const key of keys) assert.ok(table[key], `${lang}: ${key}`)
|
package/test/refusals.test.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
13
13
|
import * as fm from '@hatiolab/figure-model'
|
|
14
14
|
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
15
15
|
import {
|
|
16
|
+
clipLossOf,
|
|
16
17
|
editRefusalOf,
|
|
17
18
|
inDeclaredRoom,
|
|
18
19
|
inputRangeRefusalOf,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
placedRefusalOf,
|
|
23
24
|
placementRefusalOf,
|
|
24
25
|
recipeRefusalOf,
|
|
26
|
+
refusalRecordOf,
|
|
25
27
|
removalNoticeOf,
|
|
26
28
|
repeatPitchRefusalOf,
|
|
27
29
|
sizeRangeRefusalOf
|
|
@@ -133,11 +135,7 @@ test('공간 선언에 든 부품 — 한 번 눌러 지우고, 공간은 남은
|
|
|
133
135
|
const after = fm.editFigurePart(a, { kind: 'remove-part', target: 'stray', refitRoom: true })
|
|
134
136
|
assert.equal(after.document.model.nodes.some(n => n.id === 'stray'), false)
|
|
135
137
|
assert.ok(after.occupancy?.bounds, '공간은 남은 부품으로 다시 선언된다')
|
|
136
|
-
|
|
137
|
-
const remove = editor.slice(editor.indexOf(' private removePart('), editor.indexOf(' private toggleHiddenPart('))
|
|
138
|
-
assert.match(remove, /if \(!refitRoom && inDeclaredRoom\(error\)\) \{\s*this\.removePart\(id, true\)/, '거절 없이 곧바로 다시 맞춰 지운다')
|
|
139
|
-
assert.match(remove, /figure\.text\.removal-space-refitted/, '그렇다고 한 줄')
|
|
140
|
-
assert.doesNotMatch(remove, /this\.retry = \{/, '묻거나 권하지 않는다')
|
|
138
|
+
/* Removed in one click, nothing asked, the space fitted to the parts left and said so: parts.check.mjs. */
|
|
141
139
|
assert.ok(ko['figure.text.removal-space-refitted'])
|
|
142
140
|
})
|
|
143
141
|
|
|
@@ -153,11 +151,7 @@ test('배치를 바꿀 수 없는 까닭 — 평면에 붙은 부품과 새 평
|
|
|
153
151
|
assert.ok(said.includes('post (bottom)') && said.includes('벽 기반') && said.includes('back'), said)
|
|
154
152
|
assert.equal(editRefusalOf(refused, say), said, '편집 거절의 말도 같다')
|
|
155
153
|
assert.equal(placementRefusalOf(refusalOf(() => act(box(), declaring({ min: 0, max: 4000 }))), say), null)
|
|
156
|
-
/* The placement list sends its command through the page
|
|
157
|
-
const page = readFileSync(fileURLToPath(new URL('../client/pages/figure-modeller-page.ts', import.meta.url)), 'utf8')
|
|
158
|
-
assert.ok(page.includes('editRefusalOf(error, (key, params) => i18next.t(key, params))'), '페이지 경로도 사람의 말로')
|
|
159
|
-
const props = readFileSync(fileURLToPath(new URL('../client/modeller/figure-properties.ts', import.meta.url)), 'utf8')
|
|
160
|
-
assert.ok(props.includes('select.value = current'), '거절되면 목록이 원래 배치로 돌아온다')
|
|
154
|
+
/* The placement list sends its command through the page: said there in words, and the list back on the placement it has (product-page.check.mjs). */
|
|
161
155
|
})
|
|
162
156
|
|
|
163
157
|
test('범위 밖 값은 받을 수 있는 범위를 숫자로 말한다 — 커널의 영어가 아니다 (Figure 레인 :3300, 2026-09-26)', () => {
|
|
@@ -244,6 +238,20 @@ test('기즈모로 옮길 수 없는 부품 — 관절이 자리를 정하는지
|
|
|
244
238
|
assert.equal(placedRefusalOf(refusalOf(() => act(conveyor(), { kind: 'set-repeat', part: 'frame', pitch: 10 })), '프레임', keyed), null, '다른 EDIT_TARGET 은 이 말이 아니다')
|
|
245
239
|
})
|
|
246
240
|
|
|
241
|
+
test('옮겨 적은 거절 — 기록은 커널의 거절과 같은 모양이라 같은 말로 읽힌다 (ADR-0127)', () => {
|
|
242
|
+
const a = act(fm.addFigureBox(fm.createFigureAsset('Post'), { id: 'post', size: { x: 100, y: 1000, z: 100 }, position: { x: 0, y: 0, z: 0 } }), {
|
|
243
|
+
kind: 'attach',
|
|
244
|
+
part: 'post',
|
|
245
|
+
face: 'bottom',
|
|
246
|
+
to: { plane: 'mounting-plane' }
|
|
247
|
+
})
|
|
248
|
+
const refused = refusalOf(() => act(a, { kind: 'declare-occupancy', proposal: fm.proposeFigureOccupancy(a), placement: 'wall' }))
|
|
249
|
+
const record = refusalRecordOf(refused)
|
|
250
|
+
assert.ok(record.details, 'its details kept')
|
|
251
|
+
assert.equal(editRefusalOf(record, say), editRefusalOf(refused, say))
|
|
252
|
+
assert.equal(clipLossOf(record), null, 'no clips to ask about')
|
|
253
|
+
})
|
|
254
|
+
|
|
247
255
|
test('부품 이름 — 이름 규칙은 커널 한 벌이고, 화면은 거절의 값으로 말한다 (ADR-0127)', () => {
|
|
248
256
|
const rename = (asset: FigureAsset, target: string, name: string) => refusalOf(() => fm.editFigurePart(asset, { kind: 'rename-part', target, name } as never))
|
|
249
257
|
assert.match(partNameRefusalOf(rename(conveyor(), 'frame', '1x'), keyed)!, /part-name-invalid .*"name":"1x"/)
|
package/test/theme.test.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { DEFAULT_VIEW, WORKBENCH } from '../client/modeller/figure-view.ts'
|
|
|
14
14
|
|
|
15
15
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
16
16
|
const editor = read('../client/modeller/editor.ts')
|
|
17
|
+
const styles = read('../client/modeller/editor-styles.ts')
|
|
17
18
|
|
|
18
19
|
/** Hex colours that are not a `var(--token, #fallback)` fallback. */
|
|
19
20
|
const fixed = (text: string) =>
|
|
@@ -23,6 +24,7 @@ const fixed = (text: string) =>
|
|
|
23
24
|
|
|
24
25
|
test('편집기에 박힌 색이 없다 — 토큰의 대체값만 남는다', () => {
|
|
25
26
|
assert.deepEqual(fixed(editor), [], 'editor.ts')
|
|
27
|
+
assert.deepEqual(fixed(styles), [], 'editor-styles.ts')
|
|
26
28
|
})
|
|
27
29
|
|
|
28
30
|
test('캔버스 바탕은 작업대 회색 — 판의 하늘색이라 환경 탭에서 바꾼다', () => {
|
|
@@ -30,23 +32,11 @@ test('캔버스 바탕은 작업대 회색 — 판의 하늘색이라 환경 탭
|
|
|
30
32
|
assert.equal(DEFAULT_VIEW.skyColor, WORKBENCH, '3D 바탕의 처음 값도 작업대 회색')
|
|
31
33
|
})
|
|
32
34
|
|
|
33
|
-
test('눌린 단추는 셸이 두 테마 대비를 맞춘 칸을 쓴다 — 오류는 앱의 알림이 그린다 (2026-09-27)', () => {
|
|
34
|
-
const pressed = editor.slice(editor.indexOf("button[aria-pressed='true']"), editor.indexOf('}', editor.indexOf("button[aria-pressed='true']")))
|
|
35
|
-
assert.match(pressed, /--md-sys-color-secondary-container\)/)
|
|
36
|
-
assert.match(pressed, /--md-sys-color-on-secondary-container\)/)
|
|
37
|
-
assert.match(pressed, /border-color: var\(--md-sys-color-primary\)/)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
35
|
test('편집기는 창이 아니라 자기 폭으로 접는다 — 레일이 폭을 먼저 가져간다', () => {
|
|
41
36
|
/* The widths - 200 | 1fr | 340, 160 | 300 under 860px, one column with the canvas first under 560px - are
|
|
42
37
|
ox-three-pane's, measured in @operato/layout's own test. The editor must not write the grid again. */
|
|
43
|
-
assert.doesNotMatch(editor, /@media \(max-width/, '창 폭 기준이 남아 있다 — 레일을 펼치면 편집기가 넘친다')
|
|
44
|
-
assert.doesNotMatch(editor, /grid-template-columns/, '편집기가 격자를 다시 쓰지 않는다')
|
|
38
|
+
assert.doesNotMatch(editor + styles, /@media \(max-width/, '창 폭 기준이 남아 있다 — 레일을 펼치면 편집기가 넘친다')
|
|
39
|
+
assert.doesNotMatch(editor + styles, /grid-template-columns/, '편집기가 격자를 다시 쓰지 않는다')
|
|
45
40
|
})
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
/* The bar folding the editor's own frame, and the canvas taking the width, are the browser check's (test/browser/screen.check.mjs). */
|
|
49
|
-
const aside = editor.slice(editor.indexOf(' aside {'), editor.indexOf('}', editor.indexOf(' aside {')))
|
|
50
|
-
assert.doesNotMatch(aside, /z-index/)
|
|
51
|
-
assert.doesNotMatch(aside, /border-left/, '칸 경계선은 틀이 긋는다 — 두 줄이 되지 않게')
|
|
52
|
-
})
|
|
42
|
+
/* A pressed button drawn in the shell's containers, and the side drawing no edge and stacking over nothing, are the browser check's (test/browser/screen.check.mjs). */
|