@dialpad/dialtone-css 8.80.0-next.6 → 8.80.0-next.7
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/lib/build/js/dialtone_migrate_border_radius/index.mjs +273 -0
- package/lib/build/js/dialtone_migrate_border_radius/test.mjs +422 -0
- package/lib/build/js/dialtone_migrate_typography/index.mjs +1628 -0
- package/lib/build/js/dialtone_migrate_typography/test.mjs +1020 -0
- package/lib/build/js/dialtone_migration_helper/configs/theme-to-mode.mjs +108 -0
- package/lib/build/js/dialtone_migration_helper/tests/theme-to-mode-test-examples.vue +24 -0
- package/lib/build/js/dialtone_migration_helper/tests/theme-to-mode.test.mjs +177 -0
- package/lib/build/less/components/button.less +2 -0
- package/lib/build/less/components/emoji-picker.less +10 -11
- package/lib/build/less/components/forms.less +22 -16
- package/lib/build/less/components/modal.less +8 -2
- package/lib/build/less/components/notice.less +4 -0
- package/lib/build/less/components/popover.less +1 -1
- package/lib/build/less/components/presence.less +23 -3
- package/lib/build/less/recipes/leftbar_row.less +1 -0
- package/lib/dist/dialtone-default-theme.css +67 -34
- package/lib/dist/dialtone-default-theme.min.css +1 -1
- package/lib/dist/dialtone-docs.json +1 -1
- package/lib/dist/dialtone.css +66 -34
- package/lib/dist/dialtone.min.css +1 -1
- package/lib/dist/js/dialtone_migrate_border_radius/index.mjs +273 -0
- package/lib/dist/js/dialtone_migrate_border_radius/test.mjs +422 -0
- package/lib/dist/js/dialtone_migrate_typography/index.mjs +1628 -0
- package/lib/dist/js/dialtone_migrate_typography/test.mjs +1020 -0
- package/lib/dist/js/dialtone_migration_helper/configs/theme-to-mode.mjs +108 -0
- package/lib/dist/js/dialtone_migration_helper/tests/theme-to-mode-test-examples.vue +24 -0
- package/lib/dist/js/dialtone_migration_helper/tests/theme-to-mode.test.mjs +177 -0
- package/lib/dist/tokens/tokens-101-dark.css +1 -0
- package/lib/dist/tokens/tokens-101-light.css +1 -0
- package/lib/dist/tokens/tokens-102-dark.css +1 -0
- package/lib/dist/tokens/tokens-102-light.css +1 -0
- package/lib/dist/tokens/tokens-103-dark.css +1 -0
- package/lib/dist/tokens/tokens-103-light.css +1 -0
- package/lib/dist/tokens/tokens-104-dark.css +1 -0
- package/lib/dist/tokens/tokens-104-light.css +1 -0
- package/lib/dist/tokens/tokens-105-dark.css +1 -0
- package/lib/dist/tokens/tokens-105-light.css +1 -0
- package/lib/dist/tokens/tokens-106-dark.css +1 -0
- package/lib/dist/tokens/tokens-106-light.css +1 -0
- package/lib/dist/tokens/tokens-107-dark.css +1 -0
- package/lib/dist/tokens/tokens-107-light.css +1 -0
- package/lib/dist/tokens/tokens-108-dark.css +1 -0
- package/lib/dist/tokens/tokens-108-light.css +1 -0
- package/lib/dist/tokens/tokens-109-dark.css +1 -0
- package/lib/dist/tokens/tokens-109-light.css +1 -0
- package/lib/dist/tokens/tokens-110-dark.css +1 -0
- package/lib/dist/tokens/tokens-110-light.css +1 -0
- package/lib/dist/tokens/tokens-111-dark.css +1 -0
- package/lib/dist/tokens/tokens-111-light.css +1 -0
- package/lib/dist/tokens/tokens-112-dark.css +1 -0
- package/lib/dist/tokens/tokens-112-light.css +1 -0
- package/lib/dist/tokens/tokens-113-dark.css +1 -0
- package/lib/dist/tokens/tokens-113-light.css +1 -0
- package/lib/dist/tokens/tokens-114-dark.css +1 -0
- package/lib/dist/tokens/tokens-114-light.css +1 -0
- package/lib/dist/tokens/tokens-115-dark.css +1 -0
- package/lib/dist/tokens/tokens-115-light.css +1 -0
- package/lib/dist/tokens/tokens-116-dark.css +1 -0
- package/lib/dist/tokens/tokens-116-light.css +1 -0
- package/lib/dist/tokens/tokens-117-dark.css +1 -0
- package/lib/dist/tokens/tokens-117-light.css +1 -0
- package/lib/dist/tokens/tokens-118-dark.css +1 -0
- package/lib/dist/tokens/tokens-118-light.css +1 -0
- package/lib/dist/tokens/tokens-119-dark.css +1 -0
- package/lib/dist/tokens/tokens-119-light.css +1 -0
- package/lib/dist/tokens/tokens-120-dark.css +1 -0
- package/lib/dist/tokens/tokens-120-light.css +1 -0
- package/lib/dist/tokens/tokens-121-dark.css +1 -0
- package/lib/dist/tokens/tokens-121-light.css +1 -0
- package/lib/dist/tokens/tokens-122-dark.css +1 -0
- package/lib/dist/tokens/tokens-122-light.css +1 -0
- package/lib/dist/tokens/tokens-123-dark.css +1 -0
- package/lib/dist/tokens/tokens-123-light.css +1 -0
- package/lib/dist/tokens/tokens-124-dark.css +1 -0
- package/lib/dist/tokens/tokens-124-light.css +1 -0
- package/lib/dist/tokens/tokens-125-dark.css +1 -0
- package/lib/dist/tokens/tokens-125-light.css +1 -0
- package/lib/dist/tokens/tokens-126-dark.css +1 -0
- package/lib/dist/tokens/tokens-126-light.css +1 -0
- package/lib/dist/tokens/tokens-127-dark.css +1 -0
- package/lib/dist/tokens/tokens-127-light.css +1 -0
- package/lib/dist/tokens/tokens-128-dark.css +1 -0
- package/lib/dist/tokens/tokens-128-light.css +1 -0
- package/lib/dist/tokens/tokens-129-dark.css +1 -0
- package/lib/dist/tokens/tokens-129-light.css +1 -0
- package/lib/dist/tokens/tokens-130-dark.css +1 -0
- package/lib/dist/tokens/tokens-130-light.css +1 -0
- package/lib/dist/tokens/tokens-131-dark.css +1 -0
- package/lib/dist/tokens/tokens-131-light.css +1 -0
- package/lib/dist/tokens/tokens-132-dark.css +1 -0
- package/lib/dist/tokens/tokens-132-light.css +1 -0
- package/lib/dist/tokens/tokens-133-dark.css +1 -0
- package/lib/dist/tokens/tokens-133-light.css +1 -0
- package/lib/dist/tokens/tokens-134-dark.css +1 -0
- package/lib/dist/tokens/tokens-134-light.css +1 -0
- package/lib/dist/tokens/tokens-135-dark.css +1 -0
- package/lib/dist/tokens/tokens-135-light.css +1 -0
- package/lib/dist/tokens/tokens-136-dark.css +1 -0
- package/lib/dist/tokens/tokens-136-light.css +1 -0
- package/lib/dist/tokens/tokens-137-dark.css +1 -0
- package/lib/dist/tokens/tokens-137-light.css +1 -0
- package/lib/dist/tokens/tokens-aegean-dark.css +1 -0
- package/lib/dist/tokens/tokens-aegean-light.css +1 -0
- package/lib/dist/tokens/tokens-botany-dark.css +1 -0
- package/lib/dist/tokens/tokens-botany-light.css +1 -0
- package/lib/dist/tokens/tokens-buttercream-dark.css +1 -0
- package/lib/dist/tokens/tokens-buttercream-light.css +1 -0
- package/lib/dist/tokens/tokens-ceruleo-dark.css +1 -0
- package/lib/dist/tokens/tokens-ceruleo-light.css +1 -0
- package/lib/dist/tokens/tokens-debug-dp.css +1 -0
- package/lib/dist/tokens/tokens-dp-dark.css +1 -0
- package/lib/dist/tokens/tokens-dp-light.css +1 -0
- package/lib/dist/tokens/tokens-expressive-dark.css +1 -0
- package/lib/dist/tokens/tokens-expressive-light.css +1 -0
- package/lib/dist/tokens/tokens-expressive-sm-dark.css +1 -0
- package/lib/dist/tokens/tokens-expressive-sm-light.css +1 -0
- package/lib/dist/tokens/tokens-high-desert-dark.css +1 -0
- package/lib/dist/tokens/tokens-high-desert-light.css +1 -0
- package/lib/dist/tokens/tokens-melon-dark.css +1 -0
- package/lib/dist/tokens/tokens-melon-light.css +1 -0
- package/lib/dist/tokens/tokens-plum-dark.css +1 -0
- package/lib/dist/tokens/tokens-plum-light.css +1 -0
- package/lib/dist/tokens/tokens-prota-deuter-dark.css +1 -0
- package/lib/dist/tokens/tokens-prota-deuter-light.css +1 -0
- package/lib/dist/tokens/tokens-sunflower-dark.css +1 -0
- package/lib/dist/tokens/tokens-sunflower-light.css +1 -0
- package/lib/dist/tokens/tokens-tmo-dark.css +1 -0
- package/lib/dist/tokens/tokens-tmo-light.css +1 -0
- package/lib/dist/tokens/tokens-trita-dark.css +1 -0
- package/lib/dist/tokens/tokens-trita-light.css +1 -0
- package/lib/dist/tokens/tokens-verdant-haze-dark.css +1 -0
- package/lib/dist/tokens/tokens-verdant-haze-light.css +1 -0
- package/lib/dist/tokens-docs.json +1 -1
- package/package.json +5 -3
|
@@ -0,0 +1,1020 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dialtone-migrate-typography tests.
|
|
3
|
+
*
|
|
4
|
+
* One assertion per test; data-driven via for..of where multiple cases share a concept.
|
|
5
|
+
* Run: node --test packages/dialtone-css/lib/build/js/dialtone_migrate_typography/test.mjs
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it } from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import {
|
|
11
|
+
transformContent,
|
|
12
|
+
detectImportPathFor,
|
|
13
|
+
removeMarkersForTest,
|
|
14
|
+
validateDtTextProps,
|
|
15
|
+
} from './index.mjs';
|
|
16
|
+
|
|
17
|
+
function run (input) {
|
|
18
|
+
const { transformed } = transformContent(input, { filePath: 'test.vue' });
|
|
19
|
+
return transformed;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function warnings (input) {
|
|
23
|
+
return transformContent(input, { filePath: 'test.vue' }).warnings;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Task 1 — scaffold / no-op fast path
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
describe('fast path — no typography classes', () => {
|
|
31
|
+
it('returns unchanged content when no typography class present', () => {
|
|
32
|
+
const input = '<div class="d-d-flex">Hello</div>';
|
|
33
|
+
assert.equal(run(input), input);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('emits no warnings when no typography class present', () => {
|
|
37
|
+
const input = '<div class="d-d-flex">Hello</div>';
|
|
38
|
+
assert.equal(warnings(input).length, 0);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('returns unchanged empty string', () => {
|
|
42
|
+
assert.equal(run(''), '');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Task 2 — composed class transformation
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
describe('headline → dt-text', () => {
|
|
51
|
+
const cases = [
|
|
52
|
+
// basic sizes
|
|
53
|
+
['d-headline--sm on p', '<p class="d-headline--sm">Hi</p>', '<dt-text as="p" kind="headline" size="100">Hi</dt-text>'],
|
|
54
|
+
['d-headline-small alias', '<p class="d-headline-small">Hi</p>', '<dt-text as="p" kind="headline" size="100">Hi</dt-text>'],
|
|
55
|
+
['d-headline--md on p', '<p class="d-headline--md">Hi</p>', '<dt-text as="p" kind="headline" size="300">Hi</dt-text>'],
|
|
56
|
+
['d-headline-medium alias', '<p class="d-headline-medium">Hi</p>', '<dt-text as="p" kind="headline" size="300">Hi</dt-text>'],
|
|
57
|
+
['d-headline--lg on p', '<p class="d-headline--lg">Hi</p>', '<dt-text as="p" kind="headline" size="500">Hi</dt-text>'],
|
|
58
|
+
['d-headline-large alias', '<p class="d-headline-large">Hi</p>', '<dt-text as="p" kind="headline" size="500">Hi</dt-text>'],
|
|
59
|
+
['d-headline--xl on p', '<p class="d-headline--xl">Hi</p>', '<dt-text as="p" kind="headline" size="600">Hi</dt-text>'],
|
|
60
|
+
['d-headline-extra-large alias', '<p class="d-headline-extra-large">Hi</p>', '<dt-text as="p" kind="headline" size="600">Hi</dt-text>'],
|
|
61
|
+
['d-headline--xxl on p', '<p class="d-headline--xxl">Hi</p>', '<dt-text as="p" kind="headline" size="700">Hi</dt-text>'],
|
|
62
|
+
['d-headline-extra-extra-large alias', '<p class="d-headline-extra-extra-large">Hi</p>', '<dt-text as="p" kind="headline" size="700">Hi</dt-text>'],
|
|
63
|
+
// soft variants (strength=medium)
|
|
64
|
+
['d-headline--sm-soft', '<p class="d-headline--sm-soft">Hi</p>', '<dt-text as="p" kind="headline" size="100" strength="medium">Hi</dt-text>'],
|
|
65
|
+
['d-headline-soft-small alias', '<p class="d-headline-soft-small">Hi</p>', '<dt-text as="p" kind="headline" size="100" strength="medium">Hi</dt-text>'],
|
|
66
|
+
['d-headline--lg-soft', '<p class="d-headline--lg-soft">Hi</p>', '<dt-text as="p" kind="headline" size="500" strength="medium">Hi</dt-text>'],
|
|
67
|
+
// compact variants
|
|
68
|
+
['d-headline--sm-compact', '<p class="d-headline--sm-compact">Hi</p>', '<dt-text as="p" kind="headline" size="100" density="200">Hi</dt-text>'],
|
|
69
|
+
['d-headline-compact-small alias', '<p class="d-headline-compact-small">Hi</p>', '<dt-text as="p" kind="headline" size="100" density="200">Hi</dt-text>'],
|
|
70
|
+
['d-headline--md-compact', '<p class="d-headline--md-compact">Hi</p>', '<dt-text as="p" kind="headline" size="300" density="300">Hi</dt-text>'],
|
|
71
|
+
['d-headline--lg-compact', '<p class="d-headline--lg-compact">Hi</p>', '<dt-text as="p" kind="headline" size="500" density="200">Hi</dt-text>'],
|
|
72
|
+
['d-headline--xl-compact', '<p class="d-headline--xl-compact">Hi</p>', '<dt-text as="p" kind="headline" size="600" density="100">Hi</dt-text>'],
|
|
73
|
+
['d-headline--xxl-compact (no density)', '<p class="d-headline--xxl-compact">Hi</p>', '<dt-text as="p" kind="headline" size="700">Hi</dt-text>'],
|
|
74
|
+
// soft-compact
|
|
75
|
+
['d-headline--sm-soft-compact', '<p class="d-headline--sm-soft-compact">Hi</p>', '<dt-text as="p" kind="headline" size="100" strength="medium" density="200">Hi</dt-text>'],
|
|
76
|
+
['d-headline--lg-soft-compact', '<p class="d-headline--lg-soft-compact">Hi</p>', '<dt-text as="p" kind="headline" size="500" strength="medium" density="200">Hi</dt-text>'],
|
|
77
|
+
];
|
|
78
|
+
for (const [label, input, expected] of cases) {
|
|
79
|
+
it(label, () => { assert.equal(run(input), expected); });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('body → dt-text', () => {
|
|
84
|
+
const cases = [
|
|
85
|
+
['d-body--md on p', '<p class="d-body--md">Hi</p>', '<dt-text as="p" kind="body" size="300">Hi</dt-text>'],
|
|
86
|
+
['d-body-base alias', '<p class="d-body-base">Hi</p>', '<dt-text as="p" kind="body" size="300">Hi</dt-text>'],
|
|
87
|
+
['d-body--sm on p', '<p class="d-body--sm">Hi</p>', '<dt-text as="p" kind="body" size="100">Hi</dt-text>'],
|
|
88
|
+
['d-body-small alias', '<p class="d-body-small">Hi</p>', '<dt-text as="p" kind="body" size="100">Hi</dt-text>'],
|
|
89
|
+
['d-body--md-compact', '<p class="d-body--md-compact">Hi</p>', '<dt-text as="p" kind="body" size="300" density="300">Hi</dt-text>'],
|
|
90
|
+
['d-body-compact alias', '<p class="d-body-compact">Hi</p>', '<dt-text as="p" kind="body" size="300" density="300">Hi</dt-text>'],
|
|
91
|
+
['d-body--sm-compact', '<p class="d-body--sm-compact">Hi</p>', '<dt-text as="p" kind="body" size="100" density="200">Hi</dt-text>'],
|
|
92
|
+
['d-body-compact-small alias', '<p class="d-body-compact-small">Hi</p>', '<dt-text as="p" kind="body" size="100" density="200">Hi</dt-text>'],
|
|
93
|
+
];
|
|
94
|
+
for (const [label, input, expected] of cases) {
|
|
95
|
+
it(label, () => { assert.equal(run(input), expected); });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('label → dt-text', () => {
|
|
100
|
+
const cases = [
|
|
101
|
+
['d-label--md', '<p class="d-label--md">Hi</p>', '<dt-text as="p" kind="label" size="300">Hi</dt-text>'],
|
|
102
|
+
['d-label-base alias', '<p class="d-label-base">Hi</p>', '<dt-text as="p" kind="label" size="300">Hi</dt-text>'],
|
|
103
|
+
['d-label--sm', '<p class="d-label--sm">Hi</p>', '<dt-text as="p" kind="label" size="100">Hi</dt-text>'],
|
|
104
|
+
['d-label-small alias', '<p class="d-label-small">Hi</p>', '<dt-text as="p" kind="label" size="100">Hi</dt-text>'],
|
|
105
|
+
['d-label--md-compact', '<p class="d-label--md-compact">Hi</p>', '<dt-text as="p" kind="label" size="300" density="300">Hi</dt-text>'],
|
|
106
|
+
['d-label--sm-compact', '<p class="d-label--sm-compact">Hi</p>', '<dt-text as="p" kind="label" size="100" density="200">Hi</dt-text>'],
|
|
107
|
+
['d-label--md-plain', '<p class="d-label--md-plain">Hi</p>', '<dt-text as="p" kind="label" size="300" strength="normal">Hi</dt-text>'],
|
|
108
|
+
['d-label-plain alias', '<p class="d-label-plain">Hi</p>', '<dt-text as="p" kind="label" size="300" strength="normal">Hi</dt-text>'],
|
|
109
|
+
['d-label--md-plain-compact', '<p class="d-label--md-plain-compact">Hi</p>', '<dt-text as="p" kind="label" size="300" strength="normal" density="300">Hi</dt-text>'],
|
|
110
|
+
['d-label--sm-plain', '<p class="d-label--sm-plain">Hi</p>', '<dt-text as="p" kind="label" size="100" strength="normal">Hi</dt-text>'],
|
|
111
|
+
['d-label--sm-plain-compact', '<p class="d-label--sm-plain-compact">Hi</p>', '<dt-text as="p" kind="label" size="100" strength="normal" density="200">Hi</dt-text>'],
|
|
112
|
+
];
|
|
113
|
+
for (const [label, input, expected] of cases) {
|
|
114
|
+
it(label, () => { assert.equal(run(input), expected); });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('code → dt-text', () => {
|
|
119
|
+
it('d-code--md maps to kind=code size=200', () => {
|
|
120
|
+
assert.equal(run('<p class="d-code--md">Hi</p>'), '<dt-text as="p" kind="code" size="200">Hi</dt-text>');
|
|
121
|
+
});
|
|
122
|
+
it('d-code-base alias maps to kind=code size=200', () => {
|
|
123
|
+
assert.equal(run('<p class="d-code-base">Hi</p>'), '<dt-text as="p" kind="code" size="200">Hi</dt-text>');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('flag rows — eyebrow and code-sm emit review comment, no rewrite', () => {
|
|
128
|
+
it('d-headline--eyebrow emits review comment and leaves element unchanged', () => {
|
|
129
|
+
const input = '<p class="d-headline--eyebrow">Hi</p>';
|
|
130
|
+
const out = run(input);
|
|
131
|
+
assert.ok(out.includes('<!-- dt-text-migrate: review -->'), 'should emit review comment');
|
|
132
|
+
assert.ok(out.includes('<p class="d-headline--eyebrow">'), 'should preserve original element');
|
|
133
|
+
});
|
|
134
|
+
it('d-headline-eyebrow alias emits review comment', () => {
|
|
135
|
+
const input = '<p class="d-headline-eyebrow">Hi</p>';
|
|
136
|
+
assert.ok(run(input).includes('<!-- dt-text-migrate: review -->'));
|
|
137
|
+
});
|
|
138
|
+
it('d-code--sm emits review comment and leaves element unchanged', () => {
|
|
139
|
+
const input = '<p class="d-code--sm">Hi</p>';
|
|
140
|
+
const out = run(input);
|
|
141
|
+
assert.ok(out.includes('<!-- dt-text-migrate: review -->'));
|
|
142
|
+
assert.ok(out.includes('<p class="d-code--sm">'));
|
|
143
|
+
});
|
|
144
|
+
it('d-code-small alias emits review comment', () => {
|
|
145
|
+
const input = '<p class="d-code-small">Hi</p>';
|
|
146
|
+
assert.ok(run(input).includes('<!-- dt-text-migrate: review -->'));
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('helper rows — approximate as body+density with review marker', () => {
|
|
151
|
+
it('d-helper--md rewrites to body size=300 density=300 with helper comment', () => {
|
|
152
|
+
const input = '<p class="d-helper--md">Hi</p>';
|
|
153
|
+
const out = run(input);
|
|
154
|
+
assert.ok(out.includes('kind="body"'), 'should have kind=body');
|
|
155
|
+
assert.ok(out.includes('size="300"'), 'should have size=300');
|
|
156
|
+
assert.ok(out.includes('density="300"'), 'should have density=300');
|
|
157
|
+
assert.ok(out.includes('<!-- dt-text-migrate: review helper -->'), 'should have helper comment');
|
|
158
|
+
});
|
|
159
|
+
it('d-helper--sm rewrites to body size=100 density=200 with helper comment', () => {
|
|
160
|
+
const input = '<p class="d-helper--sm">Hi</p>';
|
|
161
|
+
const out = run(input);
|
|
162
|
+
assert.ok(out.includes('kind="body"') && out.includes('size="100"') && out.includes('density="200"'));
|
|
163
|
+
assert.ok(out.includes('<!-- dt-text-migrate: review helper -->'));
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('element tags — only rewriteable tags are converted', () => {
|
|
168
|
+
const rewriteable = ['p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'label'];
|
|
169
|
+
for (const tag of rewriteable) {
|
|
170
|
+
it(`<${tag}> is rewritten to dt-text with as="${tag}"`, () => {
|
|
171
|
+
const input = `<${tag} class="d-headline--md">Hi</${tag}>`;
|
|
172
|
+
const out = run(input);
|
|
173
|
+
assert.ok(out.startsWith('<dt-text'), `should start with dt-text, got: ${out}`);
|
|
174
|
+
assert.ok(out.includes(`as="${tag}"`), `should include as="${tag}"`);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
it('<span> is rewritten without as prop (DtText default)', () => {
|
|
178
|
+
assert.equal(run('<span class="d-body--sm">x</span>'), '<dt-text kind="body" size="100">x</dt-text>');
|
|
179
|
+
});
|
|
180
|
+
// Non-rewriteable tags (a, button, li, dt-*) carrying composed typography
|
|
181
|
+
// classes are NOT auto-converted, but they DO get a review marker so the
|
|
182
|
+
// legacy class surfaces in the migration diff. (Per francisrupert review on PR #1289.)
|
|
183
|
+
it('<a> is not rewritten but gets a wrapper-tag marker', () => {
|
|
184
|
+
const out = run('<a class="d-headline--md">link</a>');
|
|
185
|
+
assert.ok(/dt-text-migrate: review composed class on wrapper tag/.test(out), `expected marker, got: ${out}`);
|
|
186
|
+
assert.ok(out.includes('<a class="d-headline--md">link</a>'), 'original element preserved');
|
|
187
|
+
});
|
|
188
|
+
it('<button> is not rewritten but gets a wrapper-tag marker', () => {
|
|
189
|
+
const out = run('<button class="d-headline--md">btn</button>');
|
|
190
|
+
assert.ok(/dt-text-migrate: review composed class on wrapper tag/.test(out), `expected marker, got: ${out}`);
|
|
191
|
+
assert.ok(out.includes('<button class="d-headline--md">btn</button>'), 'original element preserved');
|
|
192
|
+
});
|
|
193
|
+
it('<li> is not rewritten but gets a wrapper-tag marker', () => {
|
|
194
|
+
const out = run('<li class="d-headline--md">item</li>');
|
|
195
|
+
assert.ok(/dt-text-migrate: review composed class on wrapper tag/.test(out), `expected marker, got: ${out}`);
|
|
196
|
+
assert.ok(out.includes('<li class="d-headline--md">item</li>'), 'original element preserved');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('mixed classes — composed class dropped, others retained', () => {
|
|
201
|
+
it('retains non-typography class on output dt-text', () => {
|
|
202
|
+
assert.equal(
|
|
203
|
+
run('<p class="d-headline--md d-mb-200">Hi</p>'),
|
|
204
|
+
'<dt-text as="p" kind="headline" size="300" class="d-mb-200">Hi</dt-text>',
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
it('retains multiple non-typography classes', () => {
|
|
208
|
+
assert.equal(
|
|
209
|
+
run('<p class="d-headline--md d-mb-200 d-p-100">Hi</p>'),
|
|
210
|
+
'<dt-text as="p" kind="headline" size="300" class="d-mb-200 d-p-100">Hi</dt-text>',
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
it('drops only the composed class when mixed', () => {
|
|
214
|
+
const out = run('<p class="d-body--sm d-w100p">x</p>');
|
|
215
|
+
assert.ok(!out.includes('d-body--sm'), 'should drop d-body--sm');
|
|
216
|
+
assert.ok(out.includes('d-w100p'), 'should retain d-w100p');
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('multi-element file — all matches rewritten', () => {
|
|
221
|
+
it('rewrites two elements in the same file', () => {
|
|
222
|
+
const input = [
|
|
223
|
+
'<p class="d-headline--md">Title</p>',
|
|
224
|
+
'<p class="d-body--sm">Body</p>',
|
|
225
|
+
].join('\n');
|
|
226
|
+
const expected = [
|
|
227
|
+
'<dt-text as="p" kind="headline" size="300">Title</dt-text>',
|
|
228
|
+
'<dt-text as="p" kind="body" size="100">Body</dt-text>',
|
|
229
|
+
].join('\n');
|
|
230
|
+
assert.equal(run(input), expected);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Task 3 — override utility extraction + already-DtText residual lift
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
describe('override utilities on rewriteable elements', () => {
|
|
239
|
+
it('d-fw-bold on p alongside composed class → strength prop', () => {
|
|
240
|
+
assert.equal(
|
|
241
|
+
run('<p class="d-headline--md d-fw-bold">x</p>'),
|
|
242
|
+
'<dt-text as="p" kind="headline" size="300" strength="bold">x</dt-text>',
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
it('d-fc-tertiary → tone prop', () => {
|
|
246
|
+
assert.equal(
|
|
247
|
+
run('<p class="d-headline--md d-fc-tertiary">x</p>'),
|
|
248
|
+
'<dt-text as="p" kind="headline" size="300" tone="tertiary">x</dt-text>',
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
it('d-fw-bold + d-fc-tertiary together on p', () => {
|
|
252
|
+
assert.equal(
|
|
253
|
+
run('<p class="d-fw-bold d-fc-tertiary">x</p>'),
|
|
254
|
+
'<dt-text as="p" strength="bold" tone="tertiary">x</dt-text>',
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
it('d-lh-300 → density prop', () => {
|
|
258
|
+
assert.equal(
|
|
259
|
+
run('<p class="d-body--md d-lh-300">x</p>'),
|
|
260
|
+
'<dt-text as="p" kind="body" size="300" density="300">x</dt-text>',
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
it('d-truncate → truncate boolean prop (no value)', () => {
|
|
264
|
+
assert.equal(
|
|
265
|
+
run('<p class="d-body--sm d-truncate">x</p>'),
|
|
266
|
+
'<dt-text as="p" kind="body" size="100" truncate>x</dt-text>',
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
it('d-ta-left → align="start" (logical naming)', () => {
|
|
270
|
+
assert.equal(
|
|
271
|
+
run('<p class="d-body--sm d-ta-left">x</p>'),
|
|
272
|
+
'<dt-text as="p" kind="body" size="100" align="start">x</dt-text>',
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
it('d-ta-right → align="end" (logical naming)', () => {
|
|
276
|
+
assert.equal(
|
|
277
|
+
run('<p class="d-body--sm d-ta-right">x</p>'),
|
|
278
|
+
'<dt-text as="p" kind="body" size="100" align="end">x</dt-text>',
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
it('d-ta-center → align="center"', () => {
|
|
282
|
+
assert.equal(
|
|
283
|
+
run('<p class="d-body--sm d-ta-center">x</p>'),
|
|
284
|
+
'<dt-text as="p" kind="body" size="100" align="center">x</dt-text>',
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
it('d-ta-justify → align="justify"', () => {
|
|
288
|
+
assert.equal(
|
|
289
|
+
run('<p class="d-body--sm d-ta-justify">x</p>'),
|
|
290
|
+
'<dt-text as="p" kind="body" size="100" align="justify">x</dt-text>',
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
it('all 15 d-fc-* tones map correctly (spot-check)', () => {
|
|
294
|
+
const tones = [
|
|
295
|
+
['d-fc-primary', 'primary'], ['d-fc-secondary', 'secondary'], ['d-fc-muted', 'muted'],
|
|
296
|
+
['d-fc-disabled', 'disabled'], ['d-fc-placeholder', 'placeholder'],
|
|
297
|
+
['d-fc-critical', 'critical'], ['d-fc-critical-strong', 'critical-strong'],
|
|
298
|
+
['d-fc-positive', 'positive'], ['d-fc-positive-strong', 'positive-strong'],
|
|
299
|
+
['d-fc-warning', 'warning'], ['d-fc-info', 'info'], ['d-fc-info-strong', 'info-strong'],
|
|
300
|
+
['d-fc-neutral-black', 'neutral-black'], ['d-fc-neutral-white', 'neutral-white'],
|
|
301
|
+
];
|
|
302
|
+
for (const [cls, expected] of tones) {
|
|
303
|
+
const out = run(`<p class="${cls}">x</p>`);
|
|
304
|
+
assert.ok(out.includes(`tone="${expected}"`), `${cls} should produce tone="${expected}", got: ${out}`);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
it('override-only on span (no composed) → dt-text without as prop', () => {
|
|
308
|
+
assert.equal(
|
|
309
|
+
run('<span class="d-fw-bold d-fc-tertiary">x</span>'),
|
|
310
|
+
'<dt-text strength="bold" tone="tertiary">x</dt-text>',
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
it('non-rewriteable <a> with only overrides → unchanged (silent)', () => {
|
|
314
|
+
const input = '<a class="d-fw-bold">link</a>';
|
|
315
|
+
assert.equal(run(input), input);
|
|
316
|
+
});
|
|
317
|
+
it('non-rewriteable <button> with d-truncate → unchanged', () => {
|
|
318
|
+
const input = '<button class="d-truncate">btn</button>';
|
|
319
|
+
assert.equal(run(input), input);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe('--validate — DtText prop bug detection', () => {
|
|
324
|
+
it('detects object syntax on :tone', () => {
|
|
325
|
+
const issues = validateDtTextProps('<dt-text :tone="{ muted: cond }">x</dt-text>');
|
|
326
|
+
assert.equal(issues.length, 1);
|
|
327
|
+
assert.equal(issues[0].type, 'object-syntax');
|
|
328
|
+
assert.ok(issues[0].message.includes('tone'));
|
|
329
|
+
});
|
|
330
|
+
it('detects object syntax on :strength', () => {
|
|
331
|
+
const issues = validateDtTextProps('<dt-text :strength="{ bold: isUnread }">x</dt-text>');
|
|
332
|
+
assert.equal(issues.length, 1);
|
|
333
|
+
assert.equal(issues[0].type, 'object-syntax');
|
|
334
|
+
});
|
|
335
|
+
it('detects invalid density value', () => {
|
|
336
|
+
const issues = validateDtTextProps('<dt-text kind="body" density="160">x</dt-text>');
|
|
337
|
+
assert.ok(issues.some(i => i.type === 'invalid-value' && i.message.includes('density')));
|
|
338
|
+
});
|
|
339
|
+
it('detects invalid kind value', () => {
|
|
340
|
+
const issues = validateDtTextProps('<dt-text kind="title" size="md">x</dt-text>');
|
|
341
|
+
assert.ok(issues.some(i => i.type === 'invalid-value' && i.message.includes('kind')));
|
|
342
|
+
});
|
|
343
|
+
it('detects mixed CSS classes on DtText (typography utility leakage)', () => {
|
|
344
|
+
const issues = validateDtTextProps('<dt-text class="d-fw-bold d-fc-tertiary">x</dt-text>');
|
|
345
|
+
assert.ok(issues.some(i => i.type === 'mixed-class'));
|
|
346
|
+
});
|
|
347
|
+
it('clean dt-text returns 0 issues', () => {
|
|
348
|
+
const issues = validateDtTextProps('<dt-text kind="body" size="md" tone="primary">x</dt-text>');
|
|
349
|
+
assert.equal(issues.length, 0);
|
|
350
|
+
});
|
|
351
|
+
it('accepts both numeric and t-shirt size values', () => {
|
|
352
|
+
const a = validateDtTextProps('<dt-text size="300">x</dt-text>');
|
|
353
|
+
const b = validateDtTextProps('<dt-text size="md">x</dt-text>');
|
|
354
|
+
assert.equal(a.length, 0);
|
|
355
|
+
assert.equal(b.length, 0);
|
|
356
|
+
});
|
|
357
|
+
it('non-typography classes on DtText do not flag', () => {
|
|
358
|
+
const issues = validateDtTextProps('<dt-text class="my-custom d-mb-200">x</dt-text>');
|
|
359
|
+
assert.equal(issues.length, 0);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe('P3 — legacy raw-utility heading hint comment', () => {
|
|
364
|
+
it('emits hint for <h2> with d-fw-bold + d-fs-300 + d-fc-primary', () => {
|
|
365
|
+
const input = '<h2 class="d-ff-custom d-fw-bold d-fs-300 d-fc-primary">x</h2>';
|
|
366
|
+
const out = run(input);
|
|
367
|
+
assert.ok(out.includes('dt-text-migrate: legacy heading'), `should emit hint, got: ${out}`);
|
|
368
|
+
assert.ok(out.includes('kind=headline'), 'should propose kind=headline for h-tag');
|
|
369
|
+
assert.ok(out.includes('strength=bold'), 'should propose strength=bold');
|
|
370
|
+
assert.ok(out.includes('tone=primary'), 'should propose tone=primary');
|
|
371
|
+
assert.ok(out.includes('<h2 class="d-ff-custom d-fw-bold d-fs-300 d-fc-primary">'), 'element unchanged');
|
|
372
|
+
});
|
|
373
|
+
it('emits hint for <p> with d-fw-medium + d-fs-200 — non-h tag gets verify-kind hint', () => {
|
|
374
|
+
const input = '<p class="d-fw-medium d-fs-200 d-fc-secondary">x</p>';
|
|
375
|
+
const out = run(input);
|
|
376
|
+
assert.ok(out.includes('dt-text-migrate: legacy heading'));
|
|
377
|
+
assert.ok(out.includes('VERIFY'), 'non-h tag should include VERIFY-kind note');
|
|
378
|
+
});
|
|
379
|
+
it('does NOT emit hint when there is no d-fw-* (just d-fs-N alone)', () => {
|
|
380
|
+
const input = '<div class="d-fs-200">x</div>';
|
|
381
|
+
const out = run(input);
|
|
382
|
+
assert.ok(!out.includes('dt-text-migrate: legacy heading'));
|
|
383
|
+
});
|
|
384
|
+
it('does NOT emit hint when there is a composed class (Task 2 handles it)', () => {
|
|
385
|
+
const input = '<h2 class="d-headline--md d-fw-bold d-fs-300">x</h2>';
|
|
386
|
+
const out = run(input);
|
|
387
|
+
assert.ok(!out.includes('dt-text-migrate: legacy heading'));
|
|
388
|
+
});
|
|
389
|
+
it('--remove-markers cleans up the legacy-heading hint (with embedded <dt-text>)', () => {
|
|
390
|
+
const input = '<!-- dt-text-migrate: legacy heading — likely <dt-text as="h2" kind="headline" strength="bold"> (verify) --><h2>x</h2>';
|
|
391
|
+
const cleaned = removeMarkersForTest(input);
|
|
392
|
+
assert.equal(cleaned, '<h2>x</h2>');
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
describe('d-ff-mono — establishes kind=code', () => {
|
|
397
|
+
it('<span class="d-ff-mono"> → kind="code"', () => {
|
|
398
|
+
assert.equal(
|
|
399
|
+
run('<span class="d-ff-mono">x</span>'),
|
|
400
|
+
'<dt-text kind="code">x</dt-text>',
|
|
401
|
+
);
|
|
402
|
+
});
|
|
403
|
+
it('combined with d-fw-bold', () => {
|
|
404
|
+
assert.equal(
|
|
405
|
+
run('<span class="d-ff-mono d-fw-bold">x</span>'),
|
|
406
|
+
'<dt-text kind="code" strength="bold">x</dt-text>',
|
|
407
|
+
);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe('P1 — override-only without composed class: only span/p/label converted', () => {
|
|
412
|
+
it('<span class="d-fw-bold"> converts (text-as-default is safe)', () => {
|
|
413
|
+
assert.equal(
|
|
414
|
+
run('<span class="d-fw-bold">x</span>'),
|
|
415
|
+
'<dt-text strength="bold">x</dt-text>',
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
it('<p class="d-fc-secondary"> converts', () => {
|
|
419
|
+
assert.equal(
|
|
420
|
+
run('<p class="d-fc-secondary">x</p>'),
|
|
421
|
+
'<dt-text as="p" tone="secondary">x</dt-text>',
|
|
422
|
+
);
|
|
423
|
+
});
|
|
424
|
+
it('<label class="d-fw-medium"> converts', () => {
|
|
425
|
+
assert.equal(
|
|
426
|
+
run('<label class="d-fw-medium">x</label>'),
|
|
427
|
+
'<dt-text as="label" strength="medium">x</dt-text>',
|
|
428
|
+
);
|
|
429
|
+
});
|
|
430
|
+
it('<div class="d-fc-neutral-white"> stays as <div> (layout wrapper safe)', () => {
|
|
431
|
+
const input = '<div class="d-fc-neutral-white">x</div>';
|
|
432
|
+
assert.equal(run(input), input);
|
|
433
|
+
});
|
|
434
|
+
it('<h2 class="d-fw-bold d-fc-primary"> stays as <h2> (kind not deducible)', () => {
|
|
435
|
+
const input = '<h2 class="d-fw-bold d-fc-primary">x</h2>';
|
|
436
|
+
assert.equal(run(input), input);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
describe('F8 — validateDtTextProps masks inert content + quote-aware', () => {
|
|
441
|
+
it('ignores DtText inside HTML comments', () => {
|
|
442
|
+
const issues = validateDtTextProps('<!-- <dt-text kind="title">x</dt-text> -->');
|
|
443
|
+
assert.equal(issues.length, 0);
|
|
444
|
+
});
|
|
445
|
+
it('ignores DtText inside <script>', () => {
|
|
446
|
+
const issues = validateDtTextProps('<script>const x = `<dt-text kind="title">x</dt-text>`;</script>');
|
|
447
|
+
assert.equal(issues.length, 0);
|
|
448
|
+
});
|
|
449
|
+
it('still detects real bugs', () => {
|
|
450
|
+
const issues = validateDtTextProps('<dt-text kind="title">x</dt-text>');
|
|
451
|
+
assert.ok(issues.some(i => i.type === 'invalid-value'));
|
|
452
|
+
});
|
|
453
|
+
it('catches mixed-class even when > appears in a previous binding', () => {
|
|
454
|
+
const issues = validateDtTextProps('<dt-text :title="a > b" class="d-fw-bold">x</dt-text>');
|
|
455
|
+
assert.ok(issues.some(i => i.type === 'mixed-class'), 'should detect mixed class after > in binding');
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
describe('C8 — addedDtText uses count delta, not boolean presence', () => {
|
|
460
|
+
it('partial migration: existing <dt-text> + new <p class="d-headline--md"> → both kept, count grows', () => {
|
|
461
|
+
const input = '<dt-text kind="body">existing</dt-text>\n<p class="d-headline--md">new</p>';
|
|
462
|
+
const out = run(input);
|
|
463
|
+
const before = (input.match(/<dt-text\b/g) || []).length;
|
|
464
|
+
const after = (out.match(/<dt-text\b/g) || []).length;
|
|
465
|
+
assert.equal(before, 1);
|
|
466
|
+
assert.equal(after, 2);
|
|
467
|
+
// The count-delta detection is what processFile uses to gate the import-warning;
|
|
468
|
+
// we just assert here that the transform produces the expected before/after counts.
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
describe('C3 — validateDtTextProps reports correct line numbers past masked regions', () => {
|
|
473
|
+
it('line number after a large script block matches the actual source line', () => {
|
|
474
|
+
const lines = ['<script>'];
|
|
475
|
+
for (let i = 0; i < 50; i++) lines.push(' const x' + i + ' = ' + i + ';');
|
|
476
|
+
lines.push('</script>');
|
|
477
|
+
lines.push('<dt-text kind="title">x</dt-text>');
|
|
478
|
+
const issues = validateDtTextProps(lines.join('\n'));
|
|
479
|
+
assert.equal(issues.length, 1);
|
|
480
|
+
assert.equal(issues[0].line, 53, `expected line 53, got ${issues[0].line}`);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
describe('C7 — idempotent dynamic-class marker insertion', () => {
|
|
485
|
+
it('running the codemod twice produces the same output as one run', () => {
|
|
486
|
+
const input = '<p :class="{ \'d-headline--md\': cond }">x</p>';
|
|
487
|
+
const once = transformContent(input).transformed;
|
|
488
|
+
const twice = transformContent(once).transformed;
|
|
489
|
+
assert.equal(once, twice, 'second run should be a no-op');
|
|
490
|
+
});
|
|
491
|
+
it('marker count stays at 1 across multiple runs', () => {
|
|
492
|
+
const input = '<p :class="{ \'d-headline--md\': cond }">x</p>';
|
|
493
|
+
let out = input;
|
|
494
|
+
for (let i = 0; i < 3; i++) out = transformContent(out).transformed;
|
|
495
|
+
assert.equal((out.match(/review dynamic class/g) || []).length, 1);
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
describe('C1 — nested span with flagged composed class stays unwrapped', () => {
|
|
500
|
+
it('<span class="d-code--sm"> inside migrated parent is NOT rewritten', () => {
|
|
501
|
+
const out = run('<p class="d-headline--md"><span class="d-code--sm">x</span></p>');
|
|
502
|
+
assert.ok(out.includes('<span class="d-code--sm">x</span>'), `span should be preserved, got: ${out}`);
|
|
503
|
+
assert.ok(!out.includes('<dt-text class="d-code--sm">'), 'should not rewrite flagged class');
|
|
504
|
+
});
|
|
505
|
+
it('<span class="d-headline--eyebrow"> inside migrated parent stays unwrapped', () => {
|
|
506
|
+
const out = run('<p class="d-headline--md"><span class="d-headline--eyebrow">x</span></p>');
|
|
507
|
+
assert.ok(out.includes('<span class="d-headline--eyebrow">x</span>'), `should preserve, got: ${out}`);
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
describe('F4 — parseExistingProps does not false-match suffix attrs', () => {
|
|
512
|
+
it('font-kind= is not treated as kind=', () => {
|
|
513
|
+
assert.equal(
|
|
514
|
+
run('<dt-text font-kind="foo" class="d-ff-mono">x</dt-text>'),
|
|
515
|
+
'<dt-text font-kind="foo" kind="code">x</dt-text>',
|
|
516
|
+
);
|
|
517
|
+
});
|
|
518
|
+
it('wrapper-truncate= is not treated as truncate', () => {
|
|
519
|
+
assert.equal(
|
|
520
|
+
run('<dt-text wrapper-truncate="yes" class="d-truncate">x</dt-text>'),
|
|
521
|
+
'<dt-text wrapper-truncate="yes" truncate>x</dt-text>',
|
|
522
|
+
);
|
|
523
|
+
});
|
|
524
|
+
it('legitimate conflict still flagged', () => {
|
|
525
|
+
const out = run('<dt-text strength="bold" class="d-fw-normal">x</dt-text>');
|
|
526
|
+
assert.ok(out.includes('<!-- dt-text-migrate: review conflicting class -->'));
|
|
527
|
+
assert.ok(out.includes('strength="bold"'));
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
describe('F3 — depth-aware parent body in collapseNestedSpans', () => {
|
|
532
|
+
it('trailing unsafe span AFTER an inner converted dt-text still gets flagged', () => {
|
|
533
|
+
const input = '<p class="d-headline--md"><span class="d-body--sm">inner</span><span class="d-fw-bold" @click="x">trailing</span></p>';
|
|
534
|
+
const out = run(input);
|
|
535
|
+
assert.ok(out.includes('<!-- dt-text-migrate: review nested span -->'), `should flag trailing unsafe span, got: ${out}`);
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
describe('F7 — nested <span> preserved when outer span is safe-collapsed', () => {
|
|
540
|
+
it('inner <span> stays intact, outer becomes <dt-text>', () => {
|
|
541
|
+
const out = run('<p class="d-headline--md"><span class="d-fw-bold"><span>nested</span></span></p>');
|
|
542
|
+
assert.ok(out.includes('<span>nested</span>'), `inner span should be preserved, got: ${out}`);
|
|
543
|
+
assert.ok(out.includes('<dt-text strength="bold">'), 'outer span converted');
|
|
544
|
+
// Should NOT produce broken markup like <span>nested</dt-text></span>
|
|
545
|
+
assert.ok(!out.includes('</dt-text></span>'), 'should not produce mismatched tags');
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
describe('F1 — quote-aware opening-tag matching (> inside :prop binding)', () => {
|
|
550
|
+
it('<p class="d-headline--md" :title="a > b"> preserves the binding', () => {
|
|
551
|
+
assert.equal(
|
|
552
|
+
run('<p class="d-headline--md" :title="a > b">x</p>'),
|
|
553
|
+
'<dt-text as="p" kind="headline" size="300" :title="a > b">x</dt-text>',
|
|
554
|
+
);
|
|
555
|
+
});
|
|
556
|
+
it('<p :disabled="i > total" class="d-body--sm"> preserves the binding', () => {
|
|
557
|
+
assert.equal(
|
|
558
|
+
run('<p :disabled="i > total" class="d-body--sm">x</p>'),
|
|
559
|
+
'<dt-text as="p" kind="body" size="100" :disabled="i > total">x</dt-text>',
|
|
560
|
+
);
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
describe('F2 — d-fs-* marker never injects into attribute values', () => {
|
|
565
|
+
it('< inside title attr does NOT cause marker injection', () => {
|
|
566
|
+
const out = run('<div title="a < b" class="d-fs-200">x</div>');
|
|
567
|
+
// The marker must come BEFORE the <div, never inside title="..."
|
|
568
|
+
assert.ok(out.startsWith('<!-- dt-text-migrate'), 'marker should precede the tag');
|
|
569
|
+
assert.ok(out.includes('<div title="a < b" class="d-fs-200">'), 'original tag intact');
|
|
570
|
+
});
|
|
571
|
+
it('< inside v-if= does NOT cause marker injection', () => {
|
|
572
|
+
const out = run('<button v-if="rowCount < total" class="d-fs-200">x</button>');
|
|
573
|
+
assert.ok(out.includes('<button v-if="rowCount < total" class="d-fs-200">'), 'original tag intact');
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
describe('B1 — *-class="..." prop attrs do not false-positive', () => {
|
|
578
|
+
it('font-size-class="d-fs-200" on a parent <AiRecapItem> does NOT flag the parent', () => {
|
|
579
|
+
const input = '<AiRecapItem font-size-class="d-fs-200" />';
|
|
580
|
+
const out = run(input);
|
|
581
|
+
assert.ok(!out.includes('dt-text-migrate'), `should not flag, got: ${out}`);
|
|
582
|
+
});
|
|
583
|
+
it('content-class="d-fs-100" on <DtTooltip> does NOT flag the tooltip', () => {
|
|
584
|
+
const input = '<DtTooltip content-class="d-fw-normal d-fs-100 d-lh-200" />';
|
|
585
|
+
const out = run(input);
|
|
586
|
+
assert.ok(!out.includes('dt-text-migrate'), `should not flag, got: ${out}`);
|
|
587
|
+
});
|
|
588
|
+
it('heading-class="d-headline--md" on <DtListItemGroup> does NOT trigger rewrite', () => {
|
|
589
|
+
const input = '<DtListItemGroup heading-class="d-headline--md" />';
|
|
590
|
+
assert.equal(run(input), input);
|
|
591
|
+
});
|
|
592
|
+
it('label-class="d-ai-center" without typography still untouched', () => {
|
|
593
|
+
const input = '<DtToggle label-class="d-ai-center" />';
|
|
594
|
+
assert.equal(run(input), input);
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
describe('already-dt-text residual lift', () => {
|
|
599
|
+
it('lifts d-fw-bold + d-fc-tertiary from existing dt-text', () => {
|
|
600
|
+
assert.equal(
|
|
601
|
+
run('<dt-text kind="body" class="d-fw-bold d-fc-tertiary">x</dt-text>'),
|
|
602
|
+
'<dt-text kind="body" strength="bold" tone="tertiary">x</dt-text>',
|
|
603
|
+
);
|
|
604
|
+
});
|
|
605
|
+
it('lifts d-truncate boolean prop', () => {
|
|
606
|
+
assert.equal(
|
|
607
|
+
run('<dt-text kind="body" class="d-truncate">x</dt-text>'),
|
|
608
|
+
'<dt-text kind="body" truncate>x</dt-text>',
|
|
609
|
+
);
|
|
610
|
+
});
|
|
611
|
+
it('preserves existing explicit prop when class would conflict', () => {
|
|
612
|
+
const input = '<dt-text strength="bold" class="d-fw-normal">x</dt-text>';
|
|
613
|
+
const out = run(input);
|
|
614
|
+
assert.ok(out.includes('strength="bold"'), 'should preserve strength=bold');
|
|
615
|
+
assert.ok(out.includes('<!-- dt-text-migrate: review conflicting class -->'), 'should emit conflict comment');
|
|
616
|
+
});
|
|
617
|
+
it('retains unrecognised classes after lifting', () => {
|
|
618
|
+
const out = run('<dt-text kind="body" class="d-fw-bold d-mb-200">x</dt-text>');
|
|
619
|
+
assert.ok(out.includes('strength="bold"'), 'should have strength');
|
|
620
|
+
assert.ok(out.includes('d-mb-200'), 'should retain d-mb-200');
|
|
621
|
+
assert.ok(!out.includes('d-fw-bold'), 'should drop d-fw-bold');
|
|
622
|
+
});
|
|
623
|
+
it('PascalCase DtText is also lifted', () => {
|
|
624
|
+
assert.equal(
|
|
625
|
+
run('<DtText kind="body" class="d-fw-bold">x</DtText>'),
|
|
626
|
+
'<DtText kind="body" strength="bold">x</DtText>',
|
|
627
|
+
);
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// ---------------------------------------------------------------------------
|
|
632
|
+
// Task 4 — nested-span collapse + dynamic :class / d-fs-* flagging
|
|
633
|
+
// ---------------------------------------------------------------------------
|
|
634
|
+
|
|
635
|
+
describe('nested-span collapse — safe case', () => {
|
|
636
|
+
it('collapses direct child span with only recognized classes', () => {
|
|
637
|
+
assert.equal(
|
|
638
|
+
run('<p class="d-headline--md"><span class="d-fw-bold">name</span></p>'),
|
|
639
|
+
'<dt-text as="p" kind="headline" size="300"><dt-text strength="bold">name</dt-text></dt-text>',
|
|
640
|
+
);
|
|
641
|
+
});
|
|
642
|
+
it('collapses span with composed class', () => {
|
|
643
|
+
assert.equal(
|
|
644
|
+
run('<p class="d-headline--md"><span class="d-body--sm">note</span></p>'),
|
|
645
|
+
'<dt-text as="p" kind="headline" size="300"><dt-text kind="body" size="100">note</dt-text></dt-text>',
|
|
646
|
+
);
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
describe('nested-span — unsafe case receives review comment', () => {
|
|
651
|
+
it('span with @click gets review comment, parent still rewritten', () => {
|
|
652
|
+
const out = run('<p class="d-headline--md"><span class="d-fw-bold" @click="x">name</span></p>');
|
|
653
|
+
assert.ok(out.startsWith('<dt-text'), 'parent should be rewritten');
|
|
654
|
+
assert.ok(out.includes('<!-- dt-text-migrate: review nested span -->'), 'should have nested span comment');
|
|
655
|
+
assert.ok(out.includes('<span class="d-fw-bold" @click="x">'), 'unsafe span should be preserved');
|
|
656
|
+
});
|
|
657
|
+
it('span with v-if gets review comment', () => {
|
|
658
|
+
const out = run('<p class="d-headline--md"><span class="d-fw-bold" v-if="cond">x</span></p>');
|
|
659
|
+
assert.ok(out.includes('<!-- dt-text-migrate: review nested span -->'));
|
|
660
|
+
assert.ok(out.includes('<span class="d-fw-bold" v-if="cond">'));
|
|
661
|
+
});
|
|
662
|
+
it('span with id attribute gets review comment', () => {
|
|
663
|
+
const out = run('<p class="d-headline--md"><span id="foo" class="d-fw-bold">x</span></p>');
|
|
664
|
+
assert.ok(out.includes('<!-- dt-text-migrate: review nested span -->'));
|
|
665
|
+
});
|
|
666
|
+
it('span with unrecognised class gets review comment', () => {
|
|
667
|
+
const out = run('<p class="d-headline--md"><span class="d-fw-bold custom-thing">x</span></p>');
|
|
668
|
+
assert.ok(out.includes('<!-- dt-text-migrate: review nested span -->'));
|
|
669
|
+
assert.ok(out.includes('<span class="d-fw-bold custom-thing">'));
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
describe('dynamic :class flagging', () => {
|
|
674
|
+
it(':class with typography class gets review comment, element unchanged', () => {
|
|
675
|
+
const input = '<p :class="{ \'d-headline--md\': cond }">x</p>';
|
|
676
|
+
const out = run(input);
|
|
677
|
+
assert.ok(out.includes('<!-- dt-text-migrate: review dynamic class -->'), 'should flag dynamic class');
|
|
678
|
+
assert.ok(out.includes(':class='), 'should preserve :class');
|
|
679
|
+
});
|
|
680
|
+
it('v-bind:class with typography class gets review comment', () => {
|
|
681
|
+
const input = '<p v-bind:class="{ \'d-body--sm\': flag }">x</p>';
|
|
682
|
+
const out = run(input);
|
|
683
|
+
assert.ok(out.includes('<!-- dt-text-migrate: review dynamic class -->'));
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
describe('d-fs-* flagging', () => {
|
|
688
|
+
it('d-fs-* on rewritten element gets review comment, class retained', () => {
|
|
689
|
+
const out = run('<p class="d-headline--md d-fs-150">x</p>');
|
|
690
|
+
assert.ok(/dt-text-migrate: review d-fs-\d+/.test(out), 'should flag d-fs-N');
|
|
691
|
+
assert.ok(out.includes('d-fs-150'), 'should retain d-fs-150 class');
|
|
692
|
+
assert.ok(out.includes('kind="headline"'), 'should still rewrite composed class');
|
|
693
|
+
});
|
|
694
|
+
it('d-fs-* emits one comment regardless of multiple fs classes', () => {
|
|
695
|
+
const out = run('<p class="d-headline--md d-fs-100 d-fs-200">x</p>');
|
|
696
|
+
const count = (out.match(/dt-text-migrate: review d-fs-/g) || []).length;
|
|
697
|
+
assert.equal(count, 1);
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
// ---------------------------------------------------------------------------
|
|
702
|
+
// Task 5 — import detection + --remove-markers + end-to-end
|
|
703
|
+
// ---------------------------------------------------------------------------
|
|
704
|
+
|
|
705
|
+
describe('import detection', () => {
|
|
706
|
+
it('detectMissingDtTextImport returns null when DtText already imported', () => {
|
|
707
|
+
const content = `<script>\nimport { DtText } from '@/components/text';\n</script>\n<p class="d-headline--md">x</p>`;
|
|
708
|
+
const { transformed } = transformContent(content, { filePath: 'test.vue' });
|
|
709
|
+
// Should not print import warning — we verify by confirming transform happened and no marker
|
|
710
|
+
assert.ok(transformed.includes('<dt-text'), 'should still transform');
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it('uses @/components/text path when @/components/ imports exist', () => {
|
|
714
|
+
// We test the exported detectImportPathFor function
|
|
715
|
+
const content = `<script>\nimport { DtButton } from '@/components/button';\n</script>`;
|
|
716
|
+
const path = detectImportPathFor(content);
|
|
717
|
+
assert.equal(path, '@/components/text');
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
it('uses @dialpad/dialtone-vue path when dialtone-vue import exists', () => {
|
|
721
|
+
const content = `<script>\nimport { DtStack } from '@dialpad/dialtone-vue';\n</script>`;
|
|
722
|
+
const path = detectImportPathFor(content);
|
|
723
|
+
assert.equal(path, '@dialpad/dialtone-vue');
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it('falls back to @/components/text when no imports present', () => {
|
|
727
|
+
const content = `<template><p class="d-headline--md">x</p></template>`;
|
|
728
|
+
const path = detectImportPathFor(content);
|
|
729
|
+
assert.equal(path, '@/components/text');
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
describe('--remove-markers', () => {
|
|
734
|
+
it('strips all dt-text-migrate review comments from content', () => {
|
|
735
|
+
const input = [
|
|
736
|
+
'<!-- dt-text-migrate: review -->',
|
|
737
|
+
'<p class="d-headline--eyebrow">Hi</p>',
|
|
738
|
+
'<!-- dt-text-migrate: review helper --><dt-text as="p" kind="body" size="300">note</dt-text>',
|
|
739
|
+
'<!-- dt-text-migrate: review nested span --><span class="d-fw-bold">x</span>',
|
|
740
|
+
].join('\n');
|
|
741
|
+
const cleaned = removeMarkersForTest(input);
|
|
742
|
+
assert.ok(!cleaned.includes('dt-text-migrate'), 'should remove all markers');
|
|
743
|
+
assert.ok(cleaned.includes('<p class="d-headline--eyebrow">'), 'should preserve element');
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
it('handles inline comment on same line as element', () => {
|
|
747
|
+
const input = '<p><!-- dt-text-migrate: review d-fs-* --><dt-text as="p">x</dt-text></p>';
|
|
748
|
+
const cleaned = removeMarkersForTest(input);
|
|
749
|
+
assert.ok(!cleaned.includes('dt-text-migrate'));
|
|
750
|
+
assert.ok(cleaned.includes('<dt-text'));
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
describe('end-to-end fixture', () => {
|
|
755
|
+
// ~30-line representative .vue template covering all transform types
|
|
756
|
+
const input = `<template>
|
|
757
|
+
<!-- (a) headline rewrite -->
|
|
758
|
+
<p class="d-headline--md">Title</p>
|
|
759
|
+
<!-- (b) body with truncate + tone -->
|
|
760
|
+
<span class="d-body--sm d-truncate d-fc-secondary">Body</span>
|
|
761
|
+
<!-- (c) already-DtText residual lift -->
|
|
762
|
+
<dt-text kind="label" class="d-fw-bold">Label</dt-text>
|
|
763
|
+
<!-- (d) safe nested-span collapse -->
|
|
764
|
+
<p class="d-headline--md"><span class="d-fw-bold">Name</span></p>
|
|
765
|
+
<!-- (e) unsafe nested span — gets comment -->
|
|
766
|
+
<p class="d-headline--md"><span class="d-fw-bold" @click="x">Click</span></p>
|
|
767
|
+
<!-- (f) dynamic :class flag -->
|
|
768
|
+
<p :class="{ 'd-headline--md': cond }">Dyn</p>
|
|
769
|
+
<!-- (g) d-fs-* flag -->
|
|
770
|
+
<p class="d-headline--md d-fs-150">Sized</p>
|
|
771
|
+
<!-- (h) non-rewriteable <a> left alone -->
|
|
772
|
+
<a class="d-fw-bold">link</a>
|
|
773
|
+
</template>`;
|
|
774
|
+
|
|
775
|
+
it('transforms the fixture and produces expected shape', () => {
|
|
776
|
+
const { transformed } = transformContent(input, { filePath: 'fixture.vue' });
|
|
777
|
+
// (a) headline
|
|
778
|
+
assert.ok(transformed.includes('<dt-text as="p" kind="headline" size="300">Title</dt-text>'), '(a) headline');
|
|
779
|
+
// (b) body with truncate+tone
|
|
780
|
+
assert.ok(transformed.includes('kind="body" size="100" tone="secondary" truncate'), '(b) body overrides');
|
|
781
|
+
// (c) residual lift
|
|
782
|
+
assert.ok(transformed.includes('<dt-text kind="label" strength="bold">Label</dt-text>'), '(c) residual lift');
|
|
783
|
+
// (d) nested-span collapse
|
|
784
|
+
assert.ok(transformed.includes('<dt-text strength="bold">Name</dt-text>'), '(d) nested collapse');
|
|
785
|
+
// (e) unsafe nested span comment
|
|
786
|
+
assert.ok(transformed.includes('<!-- dt-text-migrate: review nested span -->'), '(e) unsafe span comment');
|
|
787
|
+
// (f) dynamic class flag
|
|
788
|
+
assert.ok(transformed.includes('<!-- dt-text-migrate: review dynamic class -->'), '(f) dynamic class');
|
|
789
|
+
// (g) d-fs-* flag
|
|
790
|
+
assert.ok(/dt-text-migrate: review d-fs-\d+/.test(transformed), '(g) d-fs-N flag');
|
|
791
|
+
// (h) <a> unchanged
|
|
792
|
+
assert.ok(transformed.includes('<a class="d-fw-bold">link</a>'), '(h) <a> unchanged');
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
describe('inert content masking — should not match in script or comments', () => {
|
|
797
|
+
it('does not transform class inside HTML comment', () => {
|
|
798
|
+
const input = '<!-- <p class="d-headline--md">hidden</p> -->';
|
|
799
|
+
assert.equal(run(input), input);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it('does not transform class inside <script>', () => {
|
|
803
|
+
const input = '<script>\nconst x = `<p class="d-headline--md">hi</p>`;\n</script>';
|
|
804
|
+
assert.equal(run(input), input);
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
it('does not transform class inside <style>', () => {
|
|
808
|
+
const input = '<style>\n.d-headline--md { color: red; }\n</style>';
|
|
809
|
+
assert.equal(run(input), input);
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
// ---------------------------------------------------------------------------
|
|
814
|
+
// Wrapper safety — composed typography classes on layout containers,
|
|
815
|
+
// custom components, and elements with block/component children. Shapes
|
|
816
|
+
// here are drawn from real firespotter occurrences flagged in PR #1289
|
|
817
|
+
// review (francisrupert). Three categories:
|
|
818
|
+
// 1. Composed class on non-rewriteable tag (dt-*, custom-element).
|
|
819
|
+
// 2. Composed class on a rewriteable tag with a layout display utility.
|
|
820
|
+
// 3. Composed class on a rewriteable tag with block/component children.
|
|
821
|
+
// ---------------------------------------------------------------------------
|
|
822
|
+
|
|
823
|
+
describe('wrapper safety — non-rewriteable tag with composed class', () => {
|
|
824
|
+
// Shape: ubervoice/.../callbar.vue:33 — <dt-stack class="… d-body--md …">
|
|
825
|
+
it('<dt-stack class="d-body--md"> emits wrapper-tag marker, tag unchanged', () => {
|
|
826
|
+
const out = run('<dt-stack class="d-body--md">x</dt-stack>');
|
|
827
|
+
assert.ok(/dt-text-migrate: review composed class on wrapper tag/.test(out), `expected marker, got: ${out}`);
|
|
828
|
+
assert.ok(out.includes('<dt-stack class="d-body--md">'), 'original tag intact');
|
|
829
|
+
});
|
|
830
|
+
// Shape: ubervoice/.../operator_contact_row.vue:28 — <dt-link class="d-label--sm-plain …">
|
|
831
|
+
it('<dt-link class="d-label--sm-plain"> emits wrapper-tag marker', () => {
|
|
832
|
+
const out = run('<dt-link class="d-label--sm-plain d-td-none">link</dt-link>');
|
|
833
|
+
assert.ok(/dt-text-migrate: review composed class on wrapper tag/.test(out));
|
|
834
|
+
assert.ok(out.includes('<dt-link class="d-label--sm-plain d-td-none">'), 'original tag intact');
|
|
835
|
+
});
|
|
836
|
+
// Shape: ubervoice/.../signup_checkout_summary.vue:291 — <dt-notice class="… d-headline--md-compact">
|
|
837
|
+
it('<dt-notice class="d-headline--md-compact"> emits wrapper-tag marker', () => {
|
|
838
|
+
const out = run('<dt-notice kind="warning" class="d-mt32 d-headline--md-compact">x</dt-notice>');
|
|
839
|
+
assert.ok(/dt-text-migrate: review composed class on wrapper tag/.test(out));
|
|
840
|
+
});
|
|
841
|
+
// Custom element (kebab-case) with composed class
|
|
842
|
+
it('<dt-recipe-message-input class="d-body--md"> emits wrapper-tag marker', () => {
|
|
843
|
+
const out = run('<dt-recipe-message-input class="d-body--md" ref="input">x</dt-recipe-message-input>');
|
|
844
|
+
assert.ok(/dt-text-migrate: review composed class on wrapper tag/.test(out));
|
|
845
|
+
});
|
|
846
|
+
// Negative — non-rewriteable tag WITHOUT composed class should not get a marker
|
|
847
|
+
it('<dt-stack class="d-d-flex"> (no composed class) is left fully alone', () => {
|
|
848
|
+
const input = '<dt-stack class="d-d-flex">x</dt-stack>';
|
|
849
|
+
assert.equal(run(input), input);
|
|
850
|
+
});
|
|
851
|
+
// Idempotency — re-running does not stack markers
|
|
852
|
+
it('marker is not duplicated when codemod runs twice', () => {
|
|
853
|
+
const once = run('<dt-stack class="d-body--md">x</dt-stack>');
|
|
854
|
+
const twice = run(once);
|
|
855
|
+
const matches = once.match(/review composed class on wrapper tag/g) || [];
|
|
856
|
+
const matchesTwice = twice.match(/review composed class on wrapper tag/g) || [];
|
|
857
|
+
assert.equal(matches.length, 1, 'first run emits exactly one marker');
|
|
858
|
+
assert.equal(matchesTwice.length, 1, 'second run does not duplicate');
|
|
859
|
+
});
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
describe('wrapper safety — rewriteable tag with layout display utility', () => {
|
|
863
|
+
// Shape: ubervoice/.../buy_license_bundles_confirmation_summary.vue:116
|
|
864
|
+
// <div class="d-d-flex d-jc-space-between d-headline--lg-compact d-bt d-bc-bold d-pt12">
|
|
865
|
+
it('<div class="d-d-flex … d-headline--lg-compact …"> emits wrapper marker, no conversion', () => {
|
|
866
|
+
const out = run('<div class="d-d-flex d-jc-space-between d-headline--lg-compact">x</div>');
|
|
867
|
+
assert.ok(/dt-text-migrate: review composed class on wrapper/.test(out), `expected marker, got: ${out}`);
|
|
868
|
+
assert.ok(!out.includes('<dt-text'), 'div must NOT be converted to dt-text');
|
|
869
|
+
assert.ok(out.includes('d-headline--lg-compact'), 'composed class preserved for manual review');
|
|
870
|
+
});
|
|
871
|
+
// Shape: ubervoice/.../billing_history.vue:28
|
|
872
|
+
// <div class="d-w100p d-h100p d-d-flex d-fd-column d-jc-center d-ai-center d-body--md">
|
|
873
|
+
it('<div class="d-d-flex d-fd-column … d-body--md"> emits wrapper marker', () => {
|
|
874
|
+
const out = run('<div class="d-w100p d-d-flex d-fd-column d-body--md">x</div>');
|
|
875
|
+
assert.ok(/dt-text-migrate: review composed class on wrapper/.test(out));
|
|
876
|
+
assert.ok(!out.includes('<dt-text'));
|
|
877
|
+
});
|
|
878
|
+
// Grid container variant
|
|
879
|
+
it('<div class="d-d-grid d-body--md"> emits wrapper marker', () => {
|
|
880
|
+
const out = run('<div class="d-d-grid d-body--md">x</div>');
|
|
881
|
+
assert.ok(/dt-text-migrate: review composed class on wrapper/.test(out));
|
|
882
|
+
assert.ok(!out.includes('<dt-text'));
|
|
883
|
+
});
|
|
884
|
+
// <p> with display utility is also a layout container — should bail
|
|
885
|
+
it('<p class="d-d-flex d-body--md"> emits wrapper marker even on <p>', () => {
|
|
886
|
+
const out = run('<p class="d-d-flex d-body--md">x</p>');
|
|
887
|
+
assert.ok(/dt-text-migrate: review composed class on wrapper/.test(out));
|
|
888
|
+
assert.ok(!out.includes('<dt-text'));
|
|
889
|
+
});
|
|
890
|
+
// Headings with display utility — should also bail
|
|
891
|
+
it('<h2 class="d-d-flex d-headline--md"> emits wrapper marker', () => {
|
|
892
|
+
const out = run('<h2 class="d-d-flex d-headline--md">x</h2>');
|
|
893
|
+
assert.ok(/dt-text-migrate: review composed class on wrapper/.test(out));
|
|
894
|
+
assert.ok(!out.includes('<dt-text'));
|
|
895
|
+
});
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
describe('wrapper safety — rewriteable tag with block/component children', () => {
|
|
899
|
+
// Shape: ubervoice/.../missed_calls_limit.vue:22 — <div class="d-body--md-compact"><dt-stack>
|
|
900
|
+
it('<div class="d-body--md-compact"> wrapping <dt-stack> emits marker, no conversion', () => {
|
|
901
|
+
const out = run('<div class="d-body--md-compact">\n<dt-stack gap="400"><span>x</span></dt-stack>\n</div>');
|
|
902
|
+
assert.ok(/dt-text-migrate: review composed class on wrapper/.test(out), `expected marker, got: ${out}`);
|
|
903
|
+
assert.ok(!/<dt-text[^>]*as="div"/.test(out), 'div must NOT be converted to dt-text');
|
|
904
|
+
});
|
|
905
|
+
// Shape: ubervoice/.../call_recording_rules_settings.vue:3
|
|
906
|
+
// <div class="d-mt12 d-mb12 d-d-flex d-ai-center d-label--md-plain"><dt-checkbox>
|
|
907
|
+
// (Caught by layout signal first, but verify wrapping a form control alone also triggers.)
|
|
908
|
+
it('<div class="d-label--md-plain"> wrapping <dt-checkbox> emits wrapper marker', () => {
|
|
909
|
+
const out = run('<div class="d-label--md-plain"><dt-checkbox label="x" /></div>');
|
|
910
|
+
assert.ok(/dt-text-migrate: review composed class on wrapper/.test(out));
|
|
911
|
+
assert.ok(!/<dt-text[^>]*as="div"/.test(out));
|
|
912
|
+
});
|
|
913
|
+
// Francis's exact reproducer from the inline review
|
|
914
|
+
it('<div class="d-body--md"> wrapping <dt-button> emits wrapper marker', () => {
|
|
915
|
+
const out = run('<div class="d-body--md"><dt-button>Save</dt-button></div>');
|
|
916
|
+
assert.ok(/dt-text-migrate: review composed class on wrapper/.test(out));
|
|
917
|
+
assert.ok(!/<dt-text[^>]*as="div"/.test(out));
|
|
918
|
+
});
|
|
919
|
+
// Block element child (sibling <p>s inside a div with composed class)
|
|
920
|
+
it('<div class="d-body--md"> wrapping two <p> children emits wrapper marker', () => {
|
|
921
|
+
const out = run('<div class="d-body--md"><p>one</p><p>two</p></div>');
|
|
922
|
+
assert.ok(/dt-text-migrate: review composed class on wrapper/.test(out));
|
|
923
|
+
assert.ok(!/<dt-text[^>]*as="div"/.test(out));
|
|
924
|
+
});
|
|
925
|
+
// Nested div child
|
|
926
|
+
it('<div class="d-body--md"> wrapping <div> child emits wrapper marker', () => {
|
|
927
|
+
const out = run('<div class="d-body--md"><div>nested</div></div>');
|
|
928
|
+
assert.ok(/dt-text-migrate: review composed class on wrapper/.test(out));
|
|
929
|
+
assert.ok(!/<dt-text[^>]*as="div"/.test(out));
|
|
930
|
+
});
|
|
931
|
+
// Interactive child
|
|
932
|
+
it('<div class="d-body--md"> wrapping native <button> emits wrapper marker', () => {
|
|
933
|
+
const out = run('<div class="d-body--md"><button>x</button></div>');
|
|
934
|
+
assert.ok(/dt-text-migrate: review composed class on wrapper/.test(out));
|
|
935
|
+
});
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
describe('wrapper safety — positive cases still convert (no false positives)', () => {
|
|
939
|
+
// Plain text-leaf div should still convert
|
|
940
|
+
it('<div class="d-body--md"> with text-only content still converts', () => {
|
|
941
|
+
assert.equal(
|
|
942
|
+
run('<div class="d-body--md">Hello</div>'),
|
|
943
|
+
'<dt-text as="div" kind="body" size="300">Hello</dt-text>',
|
|
944
|
+
);
|
|
945
|
+
});
|
|
946
|
+
// Div with allowed inline children (span, em, strong) — should still convert
|
|
947
|
+
it('<div class="d-body--md"> wrapping <span>+<em> still converts', () => {
|
|
948
|
+
const out = run('<div class="d-body--md"><span>x</span> <em>y</em></div>');
|
|
949
|
+
assert.ok(/<dt-text[^>]*as="div"/.test(out), `expected conversion, got: ${out}`);
|
|
950
|
+
assert.ok(!/review composed class on wrapper/.test(out), 'should not emit wrapper marker for inline children');
|
|
951
|
+
});
|
|
952
|
+
// <p> with text-only content (the dominant happy path) — still converts
|
|
953
|
+
it('<p class="d-body--md">Hi</p> still converts', () => {
|
|
954
|
+
assert.equal(
|
|
955
|
+
run('<p class="d-body--md">Hi</p>'),
|
|
956
|
+
'<dt-text as="p" kind="body" size="300">Hi</dt-text>',
|
|
957
|
+
);
|
|
958
|
+
});
|
|
959
|
+
// <span> with composed class still converts to plain dt-text
|
|
960
|
+
it('<span class="d-body--sm">x</span> still converts', () => {
|
|
961
|
+
assert.equal(
|
|
962
|
+
run('<span class="d-body--sm">x</span>'),
|
|
963
|
+
'<dt-text kind="body" size="100">x</dt-text>',
|
|
964
|
+
);
|
|
965
|
+
});
|
|
966
|
+
// Empty body — no children at all
|
|
967
|
+
it('<p class="d-headline--md"></p> (empty body) still converts', () => {
|
|
968
|
+
const out = run('<p class="d-headline--md"></p>');
|
|
969
|
+
assert.ok(/<dt-text[^>]*as="p"/.test(out), `expected conversion, got: ${out}`);
|
|
970
|
+
});
|
|
971
|
+
// Quote-aware child detection — tag-like strings inside quoted attribute
|
|
972
|
+
// values must not be treated as real child elements (CodeRabbit, PR #1289).
|
|
973
|
+
it('<div class="d-body--md"> with tag-like string in title attr still converts', () => {
|
|
974
|
+
const out = run('<div class="d-body--md"><span title="<dt-button>">x</span></div>');
|
|
975
|
+
assert.ok(/<dt-text[^>]*as="div"/.test(out), `expected conversion, got: ${out}`);
|
|
976
|
+
assert.ok(!/review composed class on wrapper/.test(out), 'should not flag — only attr looks like a tag');
|
|
977
|
+
});
|
|
978
|
+
it('<p class="d-body--md"> with tag-like string in data attr still converts', () => {
|
|
979
|
+
const out = run('<p class="d-body--md"><span data-foo="<button>">x</span></p>');
|
|
980
|
+
assert.ok(/<dt-text[^>]*as="p"/.test(out));
|
|
981
|
+
});
|
|
982
|
+
// Combined case — attr tag-like AND a real component sibling still bails
|
|
983
|
+
it('attr tag-like plus real component sibling still emits wrapper marker', () => {
|
|
984
|
+
const out = run('<div class="d-body--md"><span title="<button>">x</span><dt-button>y</dt-button></div>');
|
|
985
|
+
assert.ok(/review composed class on wrapper/.test(out));
|
|
986
|
+
assert.ok(!/<dt-text[^>]*as="div"/.test(out));
|
|
987
|
+
});
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
describe('wrapper safety — override path (d-fw-*, d-fc-*, etc.) with component children', () => {
|
|
991
|
+
// Override path mirrors the composed-path safety: if the rewriteable tag
|
|
992
|
+
// wraps a component/block child, skip auto-conversion. Behavior here is a
|
|
993
|
+
// silent skip — element stays as-is with the override class intact, so it
|
|
994
|
+
// remains visible in the consumer's review.
|
|
995
|
+
it('<span class="d-fw-bold"> wrapping <dt-button> is NOT converted', () => {
|
|
996
|
+
const input = '<span class="d-fw-bold"><dt-button>Save</dt-button></span>';
|
|
997
|
+
assert.equal(run(input), input);
|
|
998
|
+
});
|
|
999
|
+
it('<p class="d-fw-bold"> wrapping <dt-icon /> is NOT converted', () => {
|
|
1000
|
+
const input = '<p class="d-fw-bold"><dt-icon name="x" /></p>';
|
|
1001
|
+
assert.equal(run(input), input);
|
|
1002
|
+
});
|
|
1003
|
+
it('<label class="d-fw-medium"> wrapping <dt-checkbox /> is NOT converted', () => {
|
|
1004
|
+
const input = '<label class="d-fw-medium"><dt-checkbox label="x" /></label>';
|
|
1005
|
+
assert.equal(run(input), input);
|
|
1006
|
+
});
|
|
1007
|
+
// Positive guardrails — leaf text on override classes still converts
|
|
1008
|
+
it('<span class="d-fw-bold">Bold text</span> still converts', () => {
|
|
1009
|
+
assert.equal(
|
|
1010
|
+
run('<span class="d-fw-bold">Bold text</span>'),
|
|
1011
|
+
'<dt-text strength="bold">Bold text</dt-text>',
|
|
1012
|
+
);
|
|
1013
|
+
});
|
|
1014
|
+
it('<p class="d-fw-bold">Bold paragraph</p> still converts', () => {
|
|
1015
|
+
assert.equal(
|
|
1016
|
+
run('<p class="d-fw-bold">Bold paragraph</p>'),
|
|
1017
|
+
'<dt-text as="p" strength="bold">Bold paragraph</dt-text>',
|
|
1018
|
+
);
|
|
1019
|
+
});
|
|
1020
|
+
});
|