mgv-backoffice 1.8.0 → 1.11.0
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/README.md +74 -4
- package/dist/src/components/BaseSpinner.vue.d.ts +10 -1
- package/dist/ui-lib.css +1 -1
- package/dist/ui-lib.js +168 -147
- package/dist/ui-lib.umd.cjs +1 -1
- package/package.json +6 -2
- package/src/components/BaseSpinner.vue +76 -3
- package/src/utils/format.test.ts +129 -0
- package/src/utils/format.ts +6 -6
- package/src/utils/httpColors.test.ts +62 -0
- package/src/utils/pnl.test.ts +45 -0
- package/src/utils/sanitizeHtml.test.ts +77 -0
- package/src/utils/util.test.ts +28 -0
- package/tailwind.safelist.css +3 -1
- package/tailwind.safelist.js +11 -2
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
methodBadgeSolid,
|
|
4
|
+
methodBadgeBright,
|
|
5
|
+
statusBadgeSolid,
|
|
6
|
+
statusBadgeTinted,
|
|
7
|
+
} from './httpColors'
|
|
8
|
+
|
|
9
|
+
describe('methodBadgeSolid', () => {
|
|
10
|
+
it('maps known methods (case-insensitive)', () => {
|
|
11
|
+
expect(methodBadgeSolid('GET')).toBe('bg-blue-600')
|
|
12
|
+
expect(methodBadgeSolid('get')).toBe('bg-blue-600')
|
|
13
|
+
expect(methodBadgeSolid('DELETE')).toBe('bg-red-600')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('falls back to gray for missing/unknown methods', () => {
|
|
17
|
+
expect(methodBadgeSolid()).toBe('bg-gray-600')
|
|
18
|
+
expect(methodBadgeSolid('TRACE')).toBe('bg-gray-600')
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('methodBadgeBright', () => {
|
|
23
|
+
it('maps known methods', () => {
|
|
24
|
+
expect(methodBadgeBright('POST')).toBe('bg-green-500')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('falls back to gray', () => {
|
|
28
|
+
expect(methodBadgeBright()).toBe('bg-gray-500')
|
|
29
|
+
expect(methodBadgeBright('WAT')).toBe('bg-gray-500')
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('statusBadgeSolid', () => {
|
|
34
|
+
it('buckets by status class', () => {
|
|
35
|
+
expect(statusBadgeSolid(204)).toBe('bg-emerald-600')
|
|
36
|
+
expect(statusBadgeSolid(301)).toBe('bg-blue-600')
|
|
37
|
+
expect(statusBadgeSolid(404)).toBe('bg-amber-600')
|
|
38
|
+
expect(statusBadgeSolid(500)).toBe('bg-red-600')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('falls back to gray for missing/unknown codes', () => {
|
|
42
|
+
expect(statusBadgeSolid()).toBe('bg-gray-600')
|
|
43
|
+
expect(statusBadgeSolid(100)).toBe('bg-gray-600')
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('statusBadgeTinted', () => {
|
|
48
|
+
it('returns dark-theme classes', () => {
|
|
49
|
+
expect(statusBadgeTinted(200, true)).toBe('bg-green-500/15 text-green-400')
|
|
50
|
+
expect(statusBadgeTinted(404, true)).toBe('bg-yellow-500/15 text-yellow-400')
|
|
51
|
+
expect(statusBadgeTinted(500, true)).toBe('bg-red-500/15 text-red-400')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('returns light-theme classes', () => {
|
|
55
|
+
expect(statusBadgeTinted(200, false)).toBe('bg-green-100 text-green-800')
|
|
56
|
+
expect(statusBadgeTinted(404, false)).toBe('bg-yellow-100 text-yellow-800')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('defaults a missing status to 2xx', () => {
|
|
60
|
+
expect(statusBadgeTinted(undefined, false)).toBe('bg-green-100 text-green-800')
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { computePnL } from './pnl'
|
|
3
|
+
|
|
4
|
+
describe('computePnL', () => {
|
|
5
|
+
it('computes absolute and percent PnL for a profitable position', () => {
|
|
6
|
+
expect(computePnL({ buyPrice: 100, lastPrice: 110, filledQty: 5 })).toEqual({
|
|
7
|
+
pnlUsd: 50,
|
|
8
|
+
pnlPct: 10,
|
|
9
|
+
})
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('computes a loss', () => {
|
|
13
|
+
expect(computePnL({ buyPrice: 100, lastPrice: 90, filledQty: 2 })).toEqual({
|
|
14
|
+
pnlUsd: -20,
|
|
15
|
+
pnlPct: -10,
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('accepts numeric strings', () => {
|
|
20
|
+
expect(computePnL({ buyPrice: '100', lastPrice: '110', filledQty: '5' })).toEqual({
|
|
21
|
+
pnlUsd: 50,
|
|
22
|
+
pnlPct: 10,
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('returns nulls when any input is missing', () => {
|
|
27
|
+
const nulls = { pnlUsd: null, pnlPct: null }
|
|
28
|
+
expect(computePnL({ buyPrice: null, lastPrice: 110, filledQty: 5 })).toEqual(nulls)
|
|
29
|
+
expect(computePnL({ buyPrice: 100, lastPrice: undefined, filledQty: 5 })).toEqual(nulls)
|
|
30
|
+
expect(computePnL({ buyPrice: 100, lastPrice: 110, filledQty: null })).toEqual(nulls)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('returns nulls for non-finite inputs (guards Number(null) === 0 footgun)', () => {
|
|
34
|
+
expect(computePnL({ buyPrice: 100, lastPrice: 'abc', filledQty: 5 })).toEqual({
|
|
35
|
+
pnlUsd: null,
|
|
36
|
+
pnlPct: null,
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('returns nulls when buyPrice is not positive', () => {
|
|
41
|
+
const nulls = { pnlUsd: null, pnlPct: null }
|
|
42
|
+
expect(computePnL({ buyPrice: 0, lastPrice: 110, filledQty: 5 })).toEqual(nulls)
|
|
43
|
+
expect(computePnL({ buyPrice: -5, lastPrice: 110, filledQty: 5 })).toEqual(nulls)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { sanitizeHtml, isSafeHref } from './sanitizeHtml'
|
|
3
|
+
|
|
4
|
+
describe('sanitizeHtml', () => {
|
|
5
|
+
it('returns empty string for falsy input', () => {
|
|
6
|
+
expect(sanitizeHtml(null)).toBe('')
|
|
7
|
+
expect(sanitizeHtml(undefined)).toBe('')
|
|
8
|
+
expect(sanitizeHtml('')).toBe('')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('keeps allow-listed formatting tags', () => {
|
|
12
|
+
expect(sanitizeHtml('<b>bold</b>')).toBe('<b>bold</b>')
|
|
13
|
+
expect(sanitizeHtml('<p>para</p>')).toBe('<p>para</p>')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('drops script tags and their contents', () => {
|
|
17
|
+
expect(sanitizeHtml('<p>Hi<script>alert(1)</script></p>')).toBe('<p>Hi</p>')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('drops style/iframe contents entirely', () => {
|
|
21
|
+
expect(sanitizeHtml('<div><style>.x{}</style>text</div>')).toBe('<div>text</div>')
|
|
22
|
+
expect(sanitizeHtml('<iframe src="x"></iframe>after')).toBe('after')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('unwraps disallowed elements to their text', () => {
|
|
26
|
+
expect(sanitizeHtml('<h1>Title</h1>')).toBe('Title')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('strips inline event handlers and other attributes', () => {
|
|
30
|
+
expect(sanitizeHtml('<p onclick="evil()">hi</p>')).toBe('<p>hi</p>')
|
|
31
|
+
expect(sanitizeHtml('<span class="x" style="color:red">y</span>')).toBe('<span>y</span>')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('hardens safe anchors with rel/target', () => {
|
|
35
|
+
const out = sanitizeHtml('<a href="https://example.com">link</a>')
|
|
36
|
+
expect(out).toContain('href="https://example.com"')
|
|
37
|
+
expect(out).toContain('rel="noopener noreferrer"')
|
|
38
|
+
expect(out).toContain('target="_blank"')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('strips unsafe href schemes', () => {
|
|
42
|
+
const out = sanitizeHtml('<a href="javascript:alert(1)">x</a>')
|
|
43
|
+
expect(out).not.toContain('javascript')
|
|
44
|
+
expect(out).not.toContain('href')
|
|
45
|
+
// No surviving href means no link-hardening attributes are added.
|
|
46
|
+
expect(out).not.toContain('target')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('preserves the title attribute on anchors', () => {
|
|
50
|
+
const out = sanitizeHtml('<a href="https://x.io" title="hi">x</a>')
|
|
51
|
+
expect(out).toContain('title="hi"')
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('isSafeHref', () => {
|
|
56
|
+
it('accepts safe schemes and relative/anchor links', () => {
|
|
57
|
+
expect(isSafeHref('http://x.com')).toBe(true)
|
|
58
|
+
expect(isSafeHref('https://x.com')).toBe(true)
|
|
59
|
+
expect(isSafeHref('mailto:a@b.com')).toBe(true)
|
|
60
|
+
expect(isSafeHref('tel:+123')).toBe(true)
|
|
61
|
+
expect(isSafeHref('/relative/path')).toBe(true)
|
|
62
|
+
expect(isSafeHref('#anchor')).toBe(true)
|
|
63
|
+
expect(isSafeHref('')).toBe(true)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('rejects dangerous schemes', () => {
|
|
67
|
+
expect(isSafeHref('javascript:alert(1)')).toBe(false)
|
|
68
|
+
expect(isSafeHref('data:text/html,<script>')).toBe(false)
|
|
69
|
+
expect(isSafeHref('vbscript:msgbox')).toBe(false)
|
|
70
|
+
expect(isSafeHref('file:///etc/passwd')).toBe(false)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('is case- and whitespace-insensitive', () => {
|
|
74
|
+
expect(isSafeHref(' JAVASCRIPT:alert(1) ')).toBe(false)
|
|
75
|
+
expect(isSafeHref(' HTTPS://x.com ')).toBe(true)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { getBaseColor, getBaseColorOf } from './util'
|
|
3
|
+
import { AlertEnum } from '../enums/AlertEnum'
|
|
4
|
+
import { ColorsEnums } from '../enums/ColorsEnums'
|
|
5
|
+
|
|
6
|
+
describe('getBaseColor', () => {
|
|
7
|
+
it('maps each alert variant to a Tailwind colour name', () => {
|
|
8
|
+
expect(getBaseColor(AlertEnum.ERROR)).toBe('red')
|
|
9
|
+
expect(getBaseColor(AlertEnum.SUCCESS)).toBe('green')
|
|
10
|
+
expect(getBaseColor(AlertEnum.INFO)).toBe('gray')
|
|
11
|
+
expect(getBaseColor(AlertEnum.WARNING)).toBe('yellow')
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('getBaseColorOf', () => {
|
|
16
|
+
it('maps each colour enum to a Tailwind colour name', () => {
|
|
17
|
+
expect(getBaseColorOf(ColorsEnums.GREEN)).toBe('green')
|
|
18
|
+
expect(getBaseColorOf(ColorsEnums.RED)).toBe('red')
|
|
19
|
+
expect(getBaseColorOf(ColorsEnums.BLUE)).toBe('blue')
|
|
20
|
+
expect(getBaseColorOf(ColorsEnums.YELLOW)).toBe('yellow')
|
|
21
|
+
expect(getBaseColorOf(ColorsEnums.BLACK)).toBe('black')
|
|
22
|
+
expect(getBaseColorOf(ColorsEnums.GRAY)).toBe('gray')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('returns an empty string for NONE', () => {
|
|
26
|
+
expect(getBaseColorOf(ColorsEnums.NONE)).toBe('')
|
|
27
|
+
})
|
|
28
|
+
})
|
package/tailwind.safelist.css
CHANGED
|
@@ -86,7 +86,9 @@
|
|
|
86
86
|
|
|
87
87
|
/* ── BaseSpinner ──────────────────────────────────────────── */
|
|
88
88
|
@source inline("
|
|
89
|
-
|
|
89
|
+
w-4 h-4 w-6 h-6 w-8 h-8 w-12 h-12 border-2 border-4
|
|
90
|
+
border-gray-200 rounded-full animate-spin
|
|
91
|
+
border-t-{blue,emerald,sky,indigo,teal,purple,red,amber}-500
|
|
90
92
|
");
|
|
91
93
|
|
|
92
94
|
/* ── BaseRow ──────────────────────────────────────────────── */
|
package/tailwind.safelist.js
CHANGED
|
@@ -226,10 +226,19 @@ const safelist = [
|
|
|
226
226
|
'text-green-500',
|
|
227
227
|
|
|
228
228
|
// ── BaseSpinner ────────────────────────────────────────────
|
|
229
|
-
'
|
|
229
|
+
'w-4', 'h-4', 'w-6', 'h-6', 'w-8', 'h-8', 'w-12', 'h-12',
|
|
230
|
+
'border-2', 'border-4',
|
|
230
231
|
'border-gray-200',
|
|
231
|
-
'
|
|
232
|
+
'rounded-full',
|
|
232
233
|
'animate-spin',
|
|
234
|
+
'border-t-blue-500',
|
|
235
|
+
'border-t-emerald-500',
|
|
236
|
+
'border-t-sky-500',
|
|
237
|
+
'border-t-indigo-500',
|
|
238
|
+
'border-t-teal-500',
|
|
239
|
+
'border-t-purple-500',
|
|
240
|
+
'border-t-red-500',
|
|
241
|
+
'border-t-amber-500',
|
|
233
242
|
|
|
234
243
|
// ── BaseRow ────────────────────────────────────────────────
|
|
235
244
|
'block',
|