Skip to content

Commit 4ef2eb3

Browse files
committed
chore(ci): improve test coverage
Signed-off-by: Cory Rylan <crylan@nvidia.com>
1 parent 522e1c4 commit 4ef2eb3

5 files changed

Lines changed: 330 additions & 18 deletions

File tree

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import{html,typeTemplateResult}from'lit';
5+
import{afterEach,describe,expect,it}from'vitest';
6+
import{createFixture,elementIsStable,emulateClick,removeFixture,untilEvent}from'@internals/testing';
7+
importtype{ButtonFormControlMixinInstance}from'@nvidia-elements/forms/mixins';
8+
import{Button}from'@nvidia-elements/core/button';
9+
import{MenuItem}from'@nvidia-elements/core/menu';
10+
import{SortButton}from'@nvidia-elements/core/sort-button';
11+
import{StepsItem}from'@nvidia-elements/core/steps';
12+
import{TabsItem}from'@nvidia-elements/core/tabs';
13+
import{Tag}from'@nvidia-elements/core/tag';
14+
import'@nvidia-elements/core/button/define.js';
15+
import'@nvidia-elements/core/menu/define.js';
16+
import'@nvidia-elements/core/sort-button/define.js';
17+
import'@nvidia-elements/core/steps/define.js';
18+
import'@nvidia-elements/core/tabs/define.js';
19+
import'@nvidia-elements/core/tag/define.js';
20+
21+
interfaceButtonUsage{
22+
tag: string;
23+
expectedRole: string;
24+
expectedType?: string;
25+
template: TemplateResult;
26+
submitTemplate: TemplateResult;
27+
}
28+
29+
typeButtonElement=HTMLElement&ButtonFormControlMixinInstance&{_internals: ElementInternals};
30+
31+
constusages: ButtonUsage[]=[
32+
{
33+
tag: Button.metadata.tag,
34+
expectedRole: 'button',
35+
template: html`<nve-buttoncommandfor="target">button</nve-button>`,
36+
submitTemplate: html`<nve-buttontype="submit" name="button-name" value="button-value" commandfor="target">button</nve-button>`
37+
},
38+
{
39+
tag: MenuItem.metadata.tag,
40+
expectedRole: 'menuitem',
41+
expectedType: 'button',
42+
template: html`<nve-menu><nve-menu-itemcommandfor="target">item</nve-menu-item></nve-menu>`,
43+
submitTemplate: html`<nve-menu><nve-menu-itemtype="submit" name="button-name" value="button-value" commandfor="target">item</nve-menu-item></nve-menu>`
44+
},
45+
{
46+
tag: SortButton.metadata.tag,
47+
expectedRole: 'spinbutton',
48+
expectedType: 'button',
49+
template: html`<nve-sort-buttoncommandfor="target"></nve-sort-button>`,
50+
submitTemplate: html`<nve-sort-buttontype="submit" name="button-name" value="button-value" commandfor="target"></nve-sort-button>`
51+
},
52+
{
53+
tag: StepsItem.metadata.tag,
54+
expectedRole: 'tab',
55+
expectedType: 'button',
56+
template: html`<nve-steps><nve-steps-itemcommandfor="target">step</nve-steps-item></nve-steps>`,
57+
submitTemplate: html`<nve-steps><nve-steps-itemtype="submit" name="button-name" value="button-value" commandfor="target">step</nve-steps-item></nve-steps>`
58+
},
59+
{
60+
tag: TabsItem.metadata.tag,
61+
expectedRole: 'tab',
62+
expectedType: 'button',
63+
template: html`<nve-tabs><nve-tabs-itemcommandfor="target">tab</nve-tabs-item></nve-tabs>`,
64+
submitTemplate: html`<nve-tabs><nve-tabs-itemtype="submit" name="button-name" value="button-value" commandfor="target">tab</nve-tabs-item></nve-tabs>`
65+
},
66+
{
67+
tag: Tag.metadata.tag,
68+
expectedRole: 'button',
69+
expectedType: 'button',
70+
template: html`<nve-tagcommandfor="target">tag</nve-tag>`,
71+
submitTemplate: html`<nve-tagtype="submit" name="button-name" value="button-value" commandfor="target">tag</nve-tag>`
72+
}
73+
];
74+
75+
describe('ButtonFormControlMixin core usage',()=>{
76+
letfixture: HTMLElement;
77+
78+
afterEach(()=>{
79+
removeFixture(fixture);
80+
});
81+
82+
usages.forEach(usage=>{
83+
describe(usage.tag,()=>{
84+
it('should expose role, focus, and type behavior',async()=>{
85+
constbutton=awaitcreateButton(usage);
86+
87+
expect(button._internals.role).toBe(usage.expectedRole);
88+
expect(button.tabIndex).toBe(0);
89+
expect(button.type).toBe(usage.expectedType);
90+
});
91+
92+
it('should sync disabled and readonly states',async()=>{
93+
constbutton=awaitcreateButton(usage);
94+
95+
button.disabled=true;
96+
awaitelementIsStable(button);
97+
expect(button._internals.ariaDisabled).toBe('true');
98+
expect(button.matches(':state(disabled)')).toBe(true);
99+
expect(button.tabIndex).toBe(-1);
100+
101+
button.disabled=false;
102+
button.readonly=true;
103+
awaitelementIsStable(button);
104+
expect(button.readOnly).toBe(true);
105+
expect(button.hasAttribute('readonly')).toBe(true);
106+
expect(button._internals.role).toBe('none');
107+
expect(button._internals.ariaDisabled).toBe(null);
108+
});
109+
110+
it('should sync active, pressed, and expanded states',async()=>{
111+
constbutton=awaitcreateButton(usage);
112+
113+
button.dispatchEvent(newMouseEvent('mousedown'));
114+
expect(button.matches(':state(active)')).toBe(true);
115+
button.dispatchEvent(newMouseEvent('mouseup'));
116+
expect(button.matches(':state(active)')).toBe(false);
117+
118+
button.pressed=true;
119+
button.expanded=true;
120+
awaitelementIsStable(button);
121+
expect(button._internals.ariaPressed).toBe('true');
122+
expect(button._internals.ariaExpanded).toBe('true');
123+
});
124+
125+
it('should dispatch commands and suppress interaction while unavailable',async()=>{
126+
constbutton=awaitcreateButton(usage);
127+
consttarget=getElement<HTMLElement>(fixture,'#target');
128+
button.command='--test-command';
129+
awaitelementIsStable(button);
130+
131+
constcommand=untilEvent<Event&{command: string;source: HTMLElement}>(target,'command');
132+
emulateClick(button);
133+
expect((awaitcommand).command).toBe('--test-command');
134+
135+
letcount=0;
136+
target.addEventListener('command',()=>(count+=1));
137+
button.disabled=true;
138+
awaitelementIsStable(button);
139+
emulateClick(button);
140+
button.disabled=false;
141+
button.readOnly=true;
142+
awaitelementIsStable(button);
143+
emulateClick(button);
144+
expect(count).toBe(0);
145+
});
146+
147+
it('should submit with name and value when type is submit',async()=>{
148+
constbutton=awaitcreateSubmitButton(usage);
149+
constform=getElement<HTMLFormElement>(fixture,'form');
150+
form.addEventListener('submit',event=>event.preventDefault());
151+
152+
constsubmit=untilEvent<SubmitEvent>(form,'submit');
153+
emulateClick(button);
154+
constevent=awaitsubmit;
155+
156+
expect(event.submitter?.name).toBe('button-name');
157+
expect(event.submitter?.value).toBe('button-value');
158+
expect(event.submitter?.form).toBe(form);
159+
});
160+
});
161+
});
162+
163+
asyncfunctioncreateButton({ template, tag }: ButtonUsage){
164+
fixture=awaitcreateFixture(html`
165+
${template}
166+
<divid="target"></div>
167+
`);
168+
constbutton=getElement<ButtonElement>(fixture,tag);
169+
awaitelementIsStable(button);
170+
returnbutton;
171+
}
172+
173+
asyncfunctioncreateSubmitButton({ submitTemplate, tag }: ButtonUsage){
174+
fixture=awaitcreateFixture(html`
175+
<form>
176+
${submitTemplate}
177+
</form>
178+
<divid="target"></div>
179+
`);
180+
constbutton=getElement<ButtonElement>(fixture,tag);
181+
awaitelementIsStable(button);
182+
returnbutton;
183+
}
184+
});
185+
186+
functiongetElement<TextendsElement>(root: ParentNode,selector: string){
187+
constelement=root.querySelector<T>(selector);
188+
expect(element).toBeTruthy();
189+
returnelementasT;
190+
}

‎projects/internals/eslint/src/local/no-single-consumer-internal-base.test.js‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ test('defines rule metadata', () => {
3636
test('valid: internal abstract base with two implementation consumers',()=>{
3737
constfilename=createFile('src/internal/media-toggle-button.ts','export abstract class MediaToggleButton {}');
3838
createFile(
39-
'src/media-play-button/media-play-button.ts',
40-
"import { MediaToggleButton } from '../internal/media-toggle-button.js';\nexport class MediaPlayButton extends MediaToggleButton {}"
39+
'src/media-pause-button/media-pause-button.ts',
40+
"import { MediaToggleButton } from '../internal/media-toggle-button.js';\nexport class MediaPauseButton extends MediaToggleButton {}"
4141
);
4242
createFile(
4343
'src/media-mute-button/media-mute-button.ts',
@@ -59,8 +59,8 @@ test('valid: internal abstract base with two implementation consumers', () => {
5959
test('valid: counts named alias and namespace import subclasses',()=>{
6060
constfilename=createFile('src/internal/media-button.ts','export abstract class MediaButton {}');
6161
createFile(
62-
'src/media-play-button/media-play-button.ts',
63-
"import { MediaButton as InternalMediaButton } from '../internal/media-button.js';\nexport class MediaPlayButton extends InternalMediaButton {}"
62+
'src/media-pause-button/media-pause-button.ts',
63+
"import { MediaButton as InternalMediaButton } from '../internal/media-button.js';\nexport class MediaPauseButton extends InternalMediaButton {}"
6464
);
6565
createFile(
6666
'src/media-mute-button/media-mute-button.ts',
@@ -82,8 +82,8 @@ test('valid: counts named alias and namespace import subclasses', () => {
8282
test('valid: counts default import subclasses for default-exported bases',()=>{
8383
constfilename=createFile('src/internal/media-button.ts','export default abstract class MediaButton {}');
8484
createFile(
85-
'src/media-play-button/media-play-button.ts',
86-
"import InternalMediaButton from '../internal/media-button.js';\nexport class MediaPlayButton extends InternalMediaButton {}"
85+
'src/media-pause-button/media-pause-button.ts',
86+
"import InternalMediaButton from '../internal/media-button.js';\nexport class MediaPauseButton extends InternalMediaButton {}"
8787
);
8888
createFile(
8989
'src/media-mute-button/media-mute-button.ts',
@@ -175,8 +175,8 @@ test('valid: ignores non-internal and non-base internal classes', () => {
175175
test('invalid: internal abstract base with one importing consumer',()=>{
176176
constfilename=createFile('src/internal/media-button.ts','export abstract class MediaButton {}');
177177
createFile(
178-
'src/media-play-button/media-play-button.ts',
179-
"import { MediaButton } from '../internal/media-button.js';\nexport class MediaPlayButton extends MediaButton {}"
178+
'src/media-pause-button/media-pause-button.ts',
179+
"import { MediaButton } from '../internal/media-button.js';\nexport class MediaPauseButton extends MediaButton {}"
180180
);
181181

182182
tester.run('no-single-consumer-internal-base',noSingleConsumerInternalBase,{
@@ -204,8 +204,8 @@ test('invalid: ignores imports that do not subclass the base', () => {
204204
"import type { MediaButton } from './media-button.js';\nexport type MediaButtonLike = MediaButton;"
205205
);
206206
createFile(
207-
'src/media-play-button/media-play-button.ts',
208-
"import { MediaButton } from '../internal/media-button.js';\nexport class MediaPlayButton extends MediaButton {}"
207+
'src/media-pause-button/media-pause-button.ts',
208+
"import { MediaButton } from '../internal/media-button.js';\nexport class MediaPauseButton extends MediaButton {}"
209209
);
210210

211211
tester.run('no-single-consumer-internal-base',noSingleConsumerInternalBase,{
@@ -229,7 +229,7 @@ test('invalid: ignores imports that do not subclass the base', () => {
229229
test('invalid: internal abstract base with only a same-file subclass',()=>{
230230
constcode=`
231231
export abstract class MediaButton {}
232-
export class MediaPlayButton extends MediaButton {}
232+
export class MediaPauseButton extends MediaButton {}
233233
`;
234234
constfilename=createFile('src/internal/media-button.ts',code);
235235

‎projects/internals/metadata/src/tasks/tests.utils.test.ts‎

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,32 @@
11
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import{beforeAll,describe,expect,it}from'vitest';
5-
import{generateTestSummary,normalizePath,normalizeTestResults}from'./tests.utils.js';
4+
import{existsSync,mkdirSync,unlinkSync,writeFileSync}from'node:fs';
5+
import{afterAll,beforeAll,describe,expect,it}from'vitest';
6+
import{
7+
generateTestSummary,
8+
getCoverageResults,
9+
getCoverageTotal,
10+
normalizePath,
11+
normalizeTestResults
12+
}from'./tests.utils.js';
13+
importtype{CoverageResult}from'../types.js';
614
importtype{ProjectsTestSummary}from'../utils/reports.js';
715

16+
constcompleteCoverage: CoverageResult={
17+
lines: {total: 1,covered: 1,skipped: 0,pct: 100},
18+
statements: {total: 2,covered: 2,skipped: 0,pct: 100},
19+
branches: {total: 3,covered: 3,skipped: 0,pct: 100},
20+
functions: {total: 4,covered: 4,skipped: 0,pct: 100}
21+
};
22+
23+
constpartialCoverage: CoverageResult={
24+
lines: {total: 2,covered: 1,skipped: 0,pct: 50},
25+
statements: {total: 4,covered: 2,skipped: 0,pct: 50},
26+
branches: {total: 6,covered: 3,skipped: 0,pct: 50},
27+
functions: {total: 8,covered: 4,skipped: 0,pct: 50}
28+
};
29+
830
describe('normalizePath',()=>{
931
it('should normalize absolute path to start at /projects',()=>{
1032
constinput='/topic-branch/projects/internals/tools/src/utils.ts';
@@ -199,13 +221,79 @@ describe('normalizeTestResults', () => {
199221
});
200222
});
201223

224+
describe('getCoverageResults',()=>{
225+
it('should remove stale coverage entries for deleted source files',()=>{
226+
constexistingPath=newURL('./tests.utils.ts',import.meta.url).pathname;
227+
constdeletedPath=existingPath.replace('tests.utils.ts','deleted-source.ts');
228+
constcoverage={
229+
total: completeCoverage,
230+
[existingPath]: completeCoverage,
231+
[deletedPath]: partialCoverage
232+
};
233+
234+
expect(getCoverageResults(coverage)).toEqual([
235+
{
236+
file: 'tasks/tests.utils.ts',
237+
...completeCoverage
238+
}
239+
]);
240+
});
241+
242+
it('should preserve coverage entries outside source directories',()=>{
243+
constcoverage={
244+
total: completeCoverage,
245+
'generated.css': partialCoverage
246+
};
247+
248+
expect(getCoverageResults(coverage)).toEqual([
249+
{
250+
file: 'generated.css',
251+
...partialCoverage
252+
}
253+
]);
254+
});
255+
});
256+
257+
describe('getCoverageTotal',()=>{
258+
it('should calculate totals from filtered coverage entries',()=>{
259+
expect(getCoverageTotal([completeCoverage,partialCoverage])).toEqual({
260+
lines: {total: 3,covered: 2,skipped: 0,pct: 66.67},
261+
statements: {total: 6,covered: 4,skipped: 0,pct: 66.67},
262+
branches: {total: 9,covered: 6,skipped: 0,pct: 66.67},
263+
functions: {total: 12,covered: 8,skipped: 0,pct: 66.67}
264+
});
265+
});
266+
267+
it('should report complete percentages when there are no coverage entries',()=>{
268+
expect(getCoverageTotal([])).toEqual({
269+
lines: {total: 0,covered: 0,skipped: 0,pct: 100},
270+
statements: {total: 0,covered: 0,skipped: 0,pct: 100},
271+
branches: {total: 0,covered: 0,skipped: 0,pct: 100},
272+
functions: {total: 0,covered: 0,skipped: 0,pct: 100}
273+
});
274+
});
275+
});
276+
202277
describe('generateTestSummary',()=>{
203278
letsummary: ProjectsTestSummary;
279+
constssrSummaryPath=newURL('../../coverage/ssr/summary.json',import.meta.url);
280+
letremoveSsrSummary=false;
204281

205282
beforeAll(async()=>{
283+
if(!existsSync(ssrSummaryPath)){
284+
mkdirSync(newURL('.',ssrSummaryPath),{recursive: true});
285+
writeFileSync(ssrSummaryPath,JSON.stringify({testResults: []}));
286+
removeSsrSummary=true;
287+
}
206288
summary=awaitgenerateTestSummary();
207289
});
208290

291+
afterAll(()=>{
292+
if(removeSsrSummary){
293+
unlinkSync(ssrSummaryPath);
294+
}
295+
});
296+
209297
it('should return the metadata json',()=>{
210298
expect(summary).toBeDefined();
211299
});
@@ -235,6 +323,7 @@ describe('generateTestSummary', () => {
235323
'@nvidia-elements/lint',
236324
'@nvidia-elements/forms',
237325
'@nvidia-elements/markdown',
326+
'@nvidia-elements/media',
238327
'@nvidia-elements/monaco',
239328
'@internals/metadata',
240329
'@internals/patterns',

0 commit comments

Comments
 (0)