@things-factory/shell 10.1.20 → 10.1.27
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/themes/dark.css +52 -45
- package/client/themes/layout-theme.css +8 -10
- package/client/themes/light.css +52 -45
- package/client/themes/material-theme.css +17 -0
- package/client/themes/spacing.css +15 -6
- package/client/themes/theme-base.css +5 -1
- package/client/themes/ui-tokens.css +23 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/tests/control-height-tokens.test.ts +113 -0
- package/tests/error-colour-contrast.test.ts +72 -0
- package/tests/md-sys-slot-owners.test.ts +100 -0
- package/tests/popup-buttons-class-shrinks.test.ts +83 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/shell",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.27",
|
|
4
4
|
"description": "Core module for framework",
|
|
5
5
|
"bin": {
|
|
6
6
|
"things-factory": "bin/things-factory",
|
|
@@ -126,5 +126,5 @@
|
|
|
126
126
|
"pg": "^8.7.3",
|
|
127
127
|
"sqlite3": "^5.0.8"
|
|
128
128
|
},
|
|
129
|
-
"gitHead": "
|
|
129
|
+
"gitHead": "d04dc762cacf2aac8ba0b0f5d768eca365fc09da"
|
|
130
130
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control heights: the shell theme declares the tokens, and @operato/styles declares the same names
|
|
3
|
+
* with the same defaults (ADR-0060 decision 6). The theme is the value of record; the components need
|
|
4
|
+
* a fallback because they cannot see the shell. Written in two places, so this test holds them
|
|
5
|
+
* together - change one and it fails until the other follows.
|
|
6
|
+
*
|
|
7
|
+
* Two bundles: button heights (ACTION_HEIGHT_TOKENS, ui-tokens.css) and input heights
|
|
8
|
+
* (FORM_ELEMENT_HEIGHT_TOKENS, spacing.css). Material buttons and filter chips are sized once in the
|
|
9
|
+
* theme from those tokens; Material text fields and selects keep their own size.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync } from 'fs'
|
|
13
|
+
import path from 'path'
|
|
14
|
+
|
|
15
|
+
const THEMES = path.resolve(__dirname, '../client/themes')
|
|
16
|
+
|
|
17
|
+
function stylesConstants(file: string, name: string): Record<string, number> {
|
|
18
|
+
const root = path.dirname(require.resolve('@operato/styles/package.json'))
|
|
19
|
+
const built = path.join(root, 'dist/src', file)
|
|
20
|
+
|
|
21
|
+
if (!existsSync(built)) {
|
|
22
|
+
throw new Error(`the installed @operato/styles (${root}) has no ${file} - it predates ADR-0060 decision 6`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const body = readFileSync(built, 'utf-8').match(new RegExp(`${name}\\s*=\\s*\\{([\\s\\S]*?)\\}`))
|
|
26
|
+
if (!body) throw new Error(`${name} was not found in @operato/styles ${file}`)
|
|
27
|
+
|
|
28
|
+
return Object.fromEntries([...body[1].matchAll(/'(--[a-z-]+)':\s*(\d+)/g)].map(m => [m[1], Number(m[2])]))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function themeDeclarations(file: string): Record<string, string> {
|
|
32
|
+
const css = readFileSync(path.join(THEMES, file), 'utf-8').replace(/\/\*[\s\S]*?\*\//g, '')
|
|
33
|
+
return Object.fromEntries([...css.matchAll(/(--[a-z0-9-]+)\s*:\s*([^;]+);/g)].map(m => [m[1], m[2].trim()]))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function expectPlatformSlots(constants: Record<string, number>, theme: Record<string, string>) {
|
|
37
|
+
/*
|
|
38
|
+
* Each token opens a slot for operato-suite and falls back to the platform default
|
|
39
|
+
* (ADR-0060 decision 6-1). The suite never redefines the token itself, so nothing depends on which
|
|
40
|
+
* stylesheet loads last.
|
|
41
|
+
*/
|
|
42
|
+
for (const [token, px] of Object.entries(constants)) {
|
|
43
|
+
expect({ token, value: theme[token] }).toEqual({ token, value: `var(--suite-${token.slice(2)}, ${px}px)` })
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('button height tokens', () => {
|
|
48
|
+
it('the shell theme opens every @operato/styles action height as a suite slot over the platform default', () => {
|
|
49
|
+
const constants = stylesConstants('action-heights.js', 'ACTION_HEIGHT_TOKENS')
|
|
50
|
+
|
|
51
|
+
expect(Object.keys(constants).sort()).toEqual([
|
|
52
|
+
'--cell-action-height',
|
|
53
|
+
'--context-action-height',
|
|
54
|
+
'--form-action-height',
|
|
55
|
+
'--popup-action-height'
|
|
56
|
+
])
|
|
57
|
+
expectPlatformSlots(constants, themeDeclarations('ui-tokens.css'))
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('Material buttons take the form height, set once in the theme', () => {
|
|
61
|
+
const theme = themeDeclarations('material-theme.css')
|
|
62
|
+
|
|
63
|
+
for (const kind of ['filled', 'outlined', 'text', 'elevated', 'filled-tonal']) {
|
|
64
|
+
expect({ kind, value: theme[`--md-${kind}-button-container-height`] }).toEqual({
|
|
65
|
+
kind,
|
|
66
|
+
value: 'var(--form-action-height)'
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('assist chips: 36px (44px by finger) on the platform, the page action height in the operato products', () => {
|
|
72
|
+
const css = readFileSync(path.join(THEMES, 'layout-theme.css'), 'utf-8').replace(/\/\*[\s\S]*?\*\//g, '')
|
|
73
|
+
const values = [...css.matchAll(/--md-assist-chip-container-height\s*:\s*([^;]+);/g)].map(m => m[1].trim())
|
|
74
|
+
|
|
75
|
+
expect(values).toEqual([
|
|
76
|
+
'var(--suite-context-action-height, 36px)',
|
|
77
|
+
'var(--suite-context-action-height, var(--ui-tap, 44px))'
|
|
78
|
+
])
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('input height tokens', () => {
|
|
83
|
+
it('the shell theme opens small, medium and large as suite slots over the platform defaults, and nothing else', () => {
|
|
84
|
+
const constants = stylesConstants('form-element-heights.js', 'FORM_ELEMENT_HEIGHT_TOKENS')
|
|
85
|
+
const theme = themeDeclarations('spacing.css')
|
|
86
|
+
|
|
87
|
+
expect(Object.keys(constants).sort()).toEqual([
|
|
88
|
+
'--form-element-height-large',
|
|
89
|
+
'--form-element-height-medium',
|
|
90
|
+
'--form-element-height-small'
|
|
91
|
+
])
|
|
92
|
+
expectPlatformSlots(constants, theme)
|
|
93
|
+
expect(
|
|
94
|
+
Object.keys(theme)
|
|
95
|
+
.filter(name => name.startsWith('--form-element-height-'))
|
|
96
|
+
.sort()
|
|
97
|
+
).toEqual(Object.keys(constants).sort())
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('Material text fields, selects and filter chips keep their own size', () => {
|
|
101
|
+
/* Their label floats above the value inside the box and needs the room (user decision). */
|
|
102
|
+
const theme = themeDeclarations('material-theme.css')
|
|
103
|
+
const fieldSizes = Object.keys(theme).filter(name =>
|
|
104
|
+
/^--md-(outlined|filled)-(text-field|select-text-field|field|select)-.*(space|height|text-size|line-height|populated-size)$/.test(
|
|
105
|
+
name
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
expect(fieldSizes).toEqual([])
|
|
110
|
+
/* filter chips keep Material's size on the platform; ox-input-select-buttons sizes its own */
|
|
111
|
+
expect(theme['--md-filter-chip-container-height']).toBeUndefined()
|
|
112
|
+
})
|
|
113
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error text has to be readable - on its own container, as a filled chip, and on the page.
|
|
3
|
+
*
|
|
4
|
+
* Until 2026-09-17 it was not: light `error` on `surface` measured 2.95 and `on-error-container`
|
|
5
|
+
* on `error-container` 2.41; dark `on-error` on `error` and `on-error-container` on
|
|
6
|
+
* `error-container` were both 1.75. WCAG 1.4.3 asks 4.5 for text. A refusal message drawn in the
|
|
7
|
+
* error colour was the hardest line on the screen to read, on exactly the screens where reading it
|
|
8
|
+
* matters.
|
|
9
|
+
*
|
|
10
|
+
* The fix kept each colour's OKLCH hue and chroma and only moved lightness, so red is still the
|
|
11
|
+
* same red - only darker where it sits on light, and a dark red under the pink in dark mode.
|
|
12
|
+
*
|
|
13
|
+
* Every app without its own light.css / dark.css receives these values, so this is a platform-wide
|
|
14
|
+
* change on purpose: a failing contrast is a defect, not a choice.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFileSync } from 'fs'
|
|
18
|
+
import path from 'path'
|
|
19
|
+
|
|
20
|
+
const THEMES = path.resolve(__dirname, '../client/themes')
|
|
21
|
+
|
|
22
|
+
/** WCAG 1.4.3 minimum for normal text. */
|
|
23
|
+
const TEXT = 4.5
|
|
24
|
+
|
|
25
|
+
function slotsOf(file: string): Map<string, string> {
|
|
26
|
+
const css = readFileSync(path.join(THEMES, file), 'utf-8')
|
|
27
|
+
return new Map([...css.matchAll(/--md-sys-color-([a-z-]+)\s*:\s*([^;]+);/g)].map(m => [m[1], m[2].trim()]))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Accepts `#rrggbb` and `rgb(r g b)`; a `var(--x, <colour>)` resolves to its fallback. */
|
|
31
|
+
function rgbOf(value: string): [number, number, number] {
|
|
32
|
+
const fallback = value.match(/^var\(--[a-z-]+, (.*)\)$/)
|
|
33
|
+
const v = fallback ? fallback[1] : value
|
|
34
|
+
|
|
35
|
+
const hex = v.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/)
|
|
36
|
+
if (hex) return [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)]
|
|
37
|
+
|
|
38
|
+
const rgb = v.match(/^rgb\((\d+)\s+(\d+)\s+(\d+)\)$/)
|
|
39
|
+
if (rgb) return [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])]
|
|
40
|
+
|
|
41
|
+
throw new Error(`cannot read colour: ${value}`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function luminance([r, g, b]: [number, number, number]): number {
|
|
45
|
+
const lin = (c: number) => {
|
|
46
|
+
const s = c / 255
|
|
47
|
+
return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4
|
|
48
|
+
}
|
|
49
|
+
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function contrast(a: string, b: string): number {
|
|
53
|
+
const [hi, lo] = [luminance(rgbOf(a)), luminance(rgbOf(b))].sort((x, y) => y - x)
|
|
54
|
+
return (hi + 0.05) / (lo + 0.05)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** [text slot, background slot] - where the error colour is read as text. */
|
|
58
|
+
const PAIRS: [string, string][] = [
|
|
59
|
+
['on-error', 'error'],
|
|
60
|
+
['on-error-container', 'error-container'],
|
|
61
|
+
['error', 'surface'],
|
|
62
|
+
['error', 'surface-container-highest']
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
describe.each(['light.css', 'dark.css'])('error colours in shell %s', file => {
|
|
66
|
+
const slots = slotsOf(file)
|
|
67
|
+
|
|
68
|
+
it.each(PAIRS)('%s on %s reaches 4.5:1', (text, background) => {
|
|
69
|
+
expect(slots.has(text) && slots.has(background)).toBe(true)
|
|
70
|
+
expect(contrast(slots.get(text)!, slots.get(background)!)).toBeGreaterThanOrEqual(TEXT)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who may set each md-sys colour slot - decided by the shell theme, not by a document.
|
|
3
|
+
*
|
|
4
|
+
* ── Why the shell opens slots instead of apps replacing files ─────────────────
|
|
5
|
+
* The theme plugin replaces a stylesheet by file name, so an app that ships its own `light.css`
|
|
6
|
+
* takes all 49 slots: neutrals, outline, error. Products then disagree on what grey and red mean, and
|
|
7
|
+
* a fix to the shared `light.css` silently never reaches them. Ordering cannot fix it either: the
|
|
8
|
+
* plugin puts app files before the shell's, so an app's `.light { --md-sys-color-primary }` loses to
|
|
9
|
+
* the shell `light.css` behind it at equal specificity (measured on a running app bundle, 2026-09-17).
|
|
10
|
+
*
|
|
11
|
+
* So the shell declares slots as `var(--<owner>-<slot>, <shell value>)`. Custom properties resolve at
|
|
12
|
+
* compute time, so bundle order stops mattering, and a slot that is not opened cannot be reached.
|
|
13
|
+
*
|
|
14
|
+
* Three owners, one mechanism - no reliance on bundle order or specificity:
|
|
15
|
+
* --brand-<slot> a product's accent (its own brand.css) 26 slots
|
|
16
|
+
* --suite-<slot> the operato suite's shared greys (@things-factory/operato-suite) 17 slots
|
|
17
|
+
* closed error x4, shadow, scrim - literal, nobody above the platform sets them 6 slots
|
|
18
|
+
* A tenant's domain theme still wins over all of these: it writes --md-sys-color-* directly, later in
|
|
19
|
+
* the bundle. Apps with neither brand.css nor the suite get the fallbacks, the shell values of before.
|
|
20
|
+
*
|
|
21
|
+
* If md adds a slot the counts go red on purpose: someone has to decide whose it is.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { readFileSync } from 'fs'
|
|
25
|
+
import path from 'path'
|
|
26
|
+
|
|
27
|
+
const THEMES = path.resolve(__dirname, '../client/themes')
|
|
28
|
+
|
|
29
|
+
const ACCENTS = ['primary', 'secondary', 'tertiary']
|
|
30
|
+
|
|
31
|
+
const BRAND = new Set([
|
|
32
|
+
...ACCENTS.flatMap(a => [
|
|
33
|
+
a,
|
|
34
|
+
`on-${a}`,
|
|
35
|
+
`${a}-container`,
|
|
36
|
+
`on-${a}-container`,
|
|
37
|
+
`${a}-fixed`,
|
|
38
|
+
`on-${a}-fixed`,
|
|
39
|
+
`${a}-fixed-dim`,
|
|
40
|
+
`on-${a}-fixed-variant`
|
|
41
|
+
]),
|
|
42
|
+
'surface-tint',
|
|
43
|
+
'inverse-primary'
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
const SUITE = new Set([
|
|
47
|
+
'background',
|
|
48
|
+
'on-background',
|
|
49
|
+
'surface',
|
|
50
|
+
'on-surface',
|
|
51
|
+
'surface-variant',
|
|
52
|
+
'on-surface-variant',
|
|
53
|
+
'outline',
|
|
54
|
+
'outline-variant',
|
|
55
|
+
'inverse-surface',
|
|
56
|
+
'inverse-on-surface',
|
|
57
|
+
'surface-dim',
|
|
58
|
+
'surface-bright',
|
|
59
|
+
'surface-container-lowest',
|
|
60
|
+
'surface-container-low',
|
|
61
|
+
'surface-container',
|
|
62
|
+
'surface-container-high',
|
|
63
|
+
'surface-container-highest'
|
|
64
|
+
])
|
|
65
|
+
|
|
66
|
+
function slotsOf(file: string): Map<string, string> {
|
|
67
|
+
const css = readFileSync(path.join(THEMES, file), 'utf-8')
|
|
68
|
+
return new Map([...css.matchAll(/--md-sys-color-([a-z-]+)\s*:\s*([^;]+);/g)].map(m => [m[1], m[2].trim()]))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const opened = (prefix: string, slot: string) =>
|
|
72
|
+
new RegExp(`^var\\(--${prefix}-${slot}, (#[0-9a-fA-F]{3,8}|rgba?\\([^()]*\\))\\)$`)
|
|
73
|
+
|
|
74
|
+
describe.each(['light.css', 'dark.css'])('shell %s opens accent to products and neutrals to the suite', file => {
|
|
75
|
+
const slots = slotsOf(file)
|
|
76
|
+
const names = [...slots.keys()]
|
|
77
|
+
|
|
78
|
+
it('declares 49 slots: 26 brand, 17 suite, 6 closed', () => {
|
|
79
|
+
expect(BRAND.size).toBe(26)
|
|
80
|
+
expect(SUITE.size).toBe(17)
|
|
81
|
+
expect(slots.size).toBe(49)
|
|
82
|
+
expect(names.filter(s => BRAND.has(s)).length).toBe(26)
|
|
83
|
+
expect(names.filter(s => SUITE.has(s)).length).toBe(17)
|
|
84
|
+
expect(names.filter(s => !BRAND.has(s) && !SUITE.has(s)).length).toBe(6)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('reads brand slots from --brand-<slot> and suite slots from --suite-<slot>, each with a colour fallback', () => {
|
|
88
|
+
for (const [slot, value] of slots) {
|
|
89
|
+
if (BRAND.has(slot)) expect({ slot, value }).toEqual({ slot, value: expect.stringMatching(opened('brand', slot)) })
|
|
90
|
+
if (SUITE.has(slot)) expect({ slot, value }).toEqual({ slot, value: expect.stringMatching(opened('suite', slot)) })
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('keeps the closed slots literal', () => {
|
|
95
|
+
for (const [slot, value] of slots) {
|
|
96
|
+
if (BRAND.has(slot) || SUITE.has(slot)) continue
|
|
97
|
+
expect({ slot, value }).toEqual({ slot, value: expect.not.stringContaining('var(') })
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `.buttons` row of CommonPopupStyles is on its way out; this list may only shrink.
|
|
3
|
+
*
|
|
4
|
+
* ── Why ──────────────────────────────────────────────────────────────────────
|
|
5
|
+
* Popup command rows were written as markup under a CSS convention (`<div class="buttons">`), and
|
|
6
|
+
* nine different rows grew out of that. The row is now an element, `<ox-popup-actions>` from
|
|
7
|
+
* @operato/popup, which owns order, height, shape and the destructive colour.
|
|
8
|
+
*
|
|
9
|
+
* Until every popup has moved, the old rule and the element coexist. Two things keep that from
|
|
10
|
+
* becoming permanent:
|
|
11
|
+
*
|
|
12
|
+
* - a file on this list that no longer uses `.buttons` fails - the move happened, so the list must
|
|
13
|
+
* say so (a list that is never updated stops watching anything);
|
|
14
|
+
* - a file not on this list that starts using `.buttons` fails - new popups use the element.
|
|
15
|
+
*
|
|
16
|
+
* When the list is empty here, and in operato-application's own copy of this test, the `.buttons`
|
|
17
|
+
* rule itself is removed from @operato/styles.
|
|
18
|
+
*
|
|
19
|
+
* This test reads only this repository. Each repository keeps its own list.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readdirSync, readFileSync, statSync } from 'fs'
|
|
23
|
+
import path from 'path'
|
|
24
|
+
|
|
25
|
+
const REPO_ROOT = path.resolve(__dirname, '../../..')
|
|
26
|
+
const PACKAGES = path.join(REPO_ROOT, 'packages')
|
|
27
|
+
|
|
28
|
+
const SKIP = new Set(['node_modules', 'dist', 'dist-client', 'dist-server', 'dist-shared', 'coverage'])
|
|
29
|
+
|
|
30
|
+
/** Files that still render the CommonPopupStyles `.buttons` row, as of 2026-09-17. */
|
|
31
|
+
const STILL_USING = [
|
|
32
|
+
'auth-ui/client/components/app-binding-detail.ts',
|
|
33
|
+
'auth-ui/client/components/appliance-editor.ts',
|
|
34
|
+
'auth-ui/client/components/application-editor.ts',
|
|
35
|
+
'organization/client/component/approval-line-items-editor-popup.ts',
|
|
36
|
+
'organization/client/component/approval-line-selector.ts',
|
|
37
|
+
'organization/client/component/assignees-editor-popup.ts',
|
|
38
|
+
'organization/client/component/department-selector.ts',
|
|
39
|
+
'organization/client/component/recipients-editor-popup.ts'
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
function clientSources(dir: string, found: string[] = []): string[] {
|
|
43
|
+
for (const entry of readdirSync(dir)) {
|
|
44
|
+
if (SKIP.has(entry) || entry.startsWith('.')) continue
|
|
45
|
+
|
|
46
|
+
const full = path.join(dir, entry)
|
|
47
|
+
let stat
|
|
48
|
+
try {
|
|
49
|
+
stat = statSync(full)
|
|
50
|
+
} catch {
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (stat.isDirectory()) clientSources(full, found)
|
|
55
|
+
else if (entry.endsWith('.ts') && full.includes(`${path.sep}client${path.sep}`)) found.push(full)
|
|
56
|
+
}
|
|
57
|
+
return found
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Uses the row: takes CommonPopupStyles and renders an element with the `buttons` class. */
|
|
61
|
+
function usesButtonsRow(source: string): boolean {
|
|
62
|
+
return /\bCommonPopupStyles\b/.test(source) && /class\s*=\s*["'](?:[^"']*\s)?buttons(?:\s[^"']*)?["']/.test(source)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe('the CommonPopupStyles .buttons row only shrinks', () => {
|
|
66
|
+
const files = clientSources(PACKAGES)
|
|
67
|
+
const using = files
|
|
68
|
+
.filter(file => usesButtonsRow(readFileSync(file, 'utf-8')))
|
|
69
|
+
.map(file => path.relative(PACKAGES, file).split(path.sep).join('/'))
|
|
70
|
+
.sort()
|
|
71
|
+
|
|
72
|
+
it('scanned enough client sources for the answer to mean something', () => {
|
|
73
|
+
expect(files.length).toBeGreaterThanOrEqual(800)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('has no file on the list that already moved to ox-popup-actions', () => {
|
|
77
|
+
expect(STILL_USING.filter(file => !using.includes(file))).toEqual([])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('has no new file using the row', () => {
|
|
81
|
+
expect(using.filter(file => !STILL_USING.includes(file))).toEqual([])
|
|
82
|
+
})
|
|
83
|
+
})
|