Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions projects/cli/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ describe('index', () => {
expect(output).toContain('nve <cmd> [args]');
});

it('should hide the banner when output is not interactive', () => {
expect(output).not.toContain('░██████████');
expect(output).toContain('@nvidia-elements/cli');
});

it('should provide api.list', () => {
expect(output).toContain('nve api.list [format]');
});
Expand Down Expand Up @@ -163,7 +168,7 @@ describe('index', () => {
it('should reject array arguments that exceed the schema limit', () => {
const result = spawnSync(
process.execPath,
['dist/index.js', 'api.get', 'nve-card', 'nve-input', 'nve-button', 'nve-badge', 'nve-alert', 'nve-link'],
['dist/index.js', 'api.get', 'nve-card', 'nve-input', 'nve-button', 'nve-badge'],
{
timeout: 10000,
encoding: 'utf-8',
Expand All @@ -173,7 +178,7 @@ describe('index', () => {
);

expect(result.status).toBe(1);
expect(result.stderr).toContain('api.get accepts at most 5 names.');
expect(result.stderr).toContain('api.get accepts at most 3 names.');
});
});

Expand Down
14 changes: 12 additions & 2 deletions projects/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ import { hideBin } from 'yargs/helpers';
import { performance } from 'perf_hooks';
import { type ManagedToolMethod, tools, ToolSupport, type Schema } from '@internals/tools';
import { installNve } from './install.js';
import { banner, colors, exitWithCompleteToolResult, exitWithToolError, getArgValue, runAsyncTool } from './utils.js';
import {
banner,
colors,
exitWithCompleteToolResult,
exitWithToolError,
getArgValue,
isInteractiveTerminal,
runAsyncTool
} from './utils.js';
import { notifyIfUpdateAvailable } from './update.js';

export const VERSION = '0.0.0';
Expand Down Expand Up @@ -76,7 +84,9 @@ yargsInstance.command(
await exitWithToolError(result, message);
}
} else {
const greeting = colors.complete(`\x1b[?7l\n${JSON.parse(banner)}\n\n`);
const greeting = isInteractiveTerminal(process.stdout)
? colors.complete(`\x1b[?7l\n${JSON.parse(banner)}\n\n`)
: '';
console.log(
`${greeting}${colors.complete(`@nvidia-elements/cli (${BUILD_SHA})`)}\n\n${await yargsInstance.getHelp()}`
);
Expand Down
20 changes: 19 additions & 1 deletion projects/cli/src/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,24 @@ vi.mock('@inquirer/prompts', () => ({
}));

describe('utils', () => {
const stderrIsTTYDescriptor = Object.getOwnPropertyDescriptor(process.stderr, 'isTTY');

beforeEach(() => {
vi.clearAllMocks();
vi.spyOn(console, 'log').mockImplementation(() => {});
vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
Object.defineProperty(process.stderr, 'isTTY', { value: true, configurable: true });
});

afterEach(() => {
vi.restoreAllMocks();
if (stderrIsTTYDescriptor) {
Object.defineProperty(process.stderr, 'isTTY', stderrIsTTYDescriptor);
} else {
delete process.stderr.isTTY;
}
});

describe('constants', () => {
Expand Down Expand Up @@ -93,7 +101,7 @@ describe('utils', () => {

it('should return different messages on multiple calls', () => {
const messages = new Set();
// Call multiple times to increase chance of getting different messages
// Call repeatedly to increase the chance of getting different messages
for (let i = 0; i < 10; i++) {
messages.add(getSpinnerProgressMessage());
}
Expand Down Expand Up @@ -126,6 +134,16 @@ describe('utils', () => {
delete process.env.CI;
});

it('should run function without spinner when stderr is not a TTY', async () => {
Object.defineProperty(process.stderr, 'isTTY', { value: false, configurable: true });
const args = {};
const result = await runAsyncTool(args, mockFn);

expect(result).toBe('test result');
expect(mockFn).toHaveBeenCalledWith(args);
expect(ora).not.toHaveBeenCalled();
});

it('should run function without spinner when start flag is true', async () => {
const args = { start: true };
const result = await runAsyncTool(args, mockFn);
Expand Down
10 changes: 9 additions & 1 deletion projects/cli/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,20 @@ export function getSpinnerProgressMessage() {
return messages[Math.floor(Math.random() * messages.length)];
}

export function isInteractiveTerminal(stream: NodeJS.WriteStream) {
return Boolean(stream.isTTY) && !process.env.CI;
}

function shouldShowInteractiveProgress(args: Record<string, unknown>, options: RunAsyncToolOptions) {
return (options.interactiveProgress ?? true) && isInteractiveTerminal(process.stderr) && !args.start && !args.log;
}

export async function runAsyncTool(
args: Record<string, unknown>,
fn: ManagedToolMethod<unknown>,
options: RunAsyncToolOptions = {}
) {
const isInteractive = (options.interactiveProgress ?? true) && !args.start && !args.log && !process.env.CI;
const isInteractive = shouldShowInteractiveProgress(args, options);
let spinner: Ora | undefined;

const startTime = Date.now();
Expand Down
10 changes: 5 additions & 5 deletions projects/core/src/page/page-panel/page-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ import globalStyles from './page-panel.global.css?inline';
* @description Child panel for embedded panels within the page component. Typically used for left/right/bottom page slot positions.
* @entrypoint \@nvidia-elements/core/page
* @since 1.15.0
* @event open
* @event close
* @event open - Dispatched after an invoker command removes `hidden` and opens the panel.
* @event close - Dispatched after an invoker command sets `hidden` and closes the panel.
* @slot - default content slot
* @slot actions - slot for action / dismiss buttons
* @command --open - use to open the panel
* @command --close - use to close the panel
* @command --toggle - use to toggle the panel
* @command --open - Removes `hidden` and dispatches `open`.
* @command --close - Sets `hidden` and dispatches `close`.
* @command --toggle - Toggles `hidden` and dispatches `open` or `close`.
* @cssprop --background
* @cssprop --border
* @cssprop --color
Expand Down
2 changes: 1 addition & 1 deletion projects/core/src/sort-button/sort-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const nextSort = {
* @description A sort button is a control that enables users to sort a list of items in ascending or descending order.
* @since 0.11.0
* @entrypoint \@nvidia-elements/core/sort-button
* @event sort - Dispatched on sort button click, returns the current sort value and the next sort value.
* @event sort - Dispatched on activation with `detail: { value, next }`, where `value` is the current sort and `next` follows the cycle `none` → `ascending` → `descending` → `none`.
* @cssprop --width
* @cssprop --height
* @cssprop --border-radius
Expand Down
2 changes: 1 addition & 1 deletion projects/internals/tools/src/api/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('ApiService', () => {
expect((ApiService.get as ToolMethod<unknown>).metadata.name).toBe('get');
expect((ApiService.get as ToolMethod<unknown>).metadata.command).toBe('get');
expect((ApiService.get as ToolMethod<unknown>).metadata.description).toContain(
'Get documentation known components or attributes by name (nve-*). Limit: 5'
'Get documentation known components or attributes by name (nve-*). Limit: 3'
);
expect((ApiService.get as ToolMethod<unknown>).metadata.inputSchema?.properties?.names).toBeDefined();
expect((ApiService.get as ToolMethod<unknown>).metadata.inputSchema?.required).toContain('names');
Expand Down
2 changes: 1 addition & 1 deletion projects/internals/tools/src/api/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { service, tool } from '../internal/tools.js';
import { getElementImports, markdownDescription } from '../internal/utils.js';
import { eslintSchema } from '../internal/schema.js';

const MAX_RESULT_LIMIT = 5;
const MAX_RESULT_LIMIT = 3;

const listToolHelpfulTip =
'Tip: Use the list tool to get a summary list of all available components and attribute APIs.';
Expand Down
4 changes: 2 additions & 2 deletions projects/internals/tools/src/examples/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('ExampleService', () => {
expect((ExamplesService.search as ToolMethod<unknown>).metadata.name).toBe('search');
expect((ExamplesService.search as ToolMethod<unknown>).metadata.command).toBe('search');
expect((ExamplesService.search as ToolMethod<unknown>).metadata.description).toBe(
'Search Elements (nve-*) pattern usage examples by name, element type, or keywords. Returns up to 5 matching examples with full template code. Hint: use the list tool to get a list of all available examples and patterns first if unsure of what to search.'
'Search Elements (nve-*) pattern usage examples by name, element type, or keywords. Returns up to 3 matching examples with full template code. Hint: use the list tool to get a list of all available examples and patterns first if unsure of what to search.'
);
expect((ExamplesService.search as ToolMethod<unknown>).metadata.inputSchema?.properties?.query).toBeDefined();
});
Expand Down Expand Up @@ -57,7 +57,7 @@ describe('ExampleService', () => {
expect((ExamplesService.search as ToolMethod<unknown>).metadata.name).toBe('search');
expect((ExamplesService.search as ToolMethod<unknown>).metadata.command).toBe('search');
expect((ExamplesService.search as ToolMethod<unknown>).metadata.description).toBe(
'Search Elements (nve-*) pattern usage examples by name, element type, or keywords. Returns up to 5 matching examples with full template code. Hint: use the list tool to get a list of all available examples and patterns first if unsure of what to search.'
'Search Elements (nve-*) pattern usage examples by name, element type, or keywords. Returns up to 3 matching examples with full template code. Hint: use the list tool to get a list of all available examples and patterns first if unsure of what to search.'
);
expect((ExamplesService.search as ToolMethod<unknown>).metadata.inputSchema?.properties?.query).toBeDefined();
});
Expand Down
2 changes: 1 addition & 1 deletion projects/internals/tools/src/examples/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { getContextExamples, renderExampleMarkdown, searchContextExamples } from
import { markdownDescription } from '../internal/utils.js';
import { eslintSchema } from '../internal/schema.js';

const MAX_RESULT_LIMIT = 5;
const MAX_RESULT_LIMIT = 3;

@service()
export class ExamplesService {
Expand Down
13 changes: 6 additions & 7 deletions projects/internals/tools/src/packages/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,24 +95,23 @@ describe('PackagesService', () => {
expect((limited as string).length).toBeLessThanOrEqual((full as string).length);
});

it('should expose the same package set in list and changelogs-unknown-package error', async () => {
const list = (await PackagesService.list()) as string;
const listSet = new Set(Array.from(list.matchAll(/^## (\S+) v/gm), m => m[1]));

it('should expose the same package set in the changelogs schema and unknown-package error', async () => {
const schema = (PackagesService.changelogsGet as ToolMethod<unknown>).metadata.inputSchema;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Type cast bypasses type safety.

PackagesService.changelogsGet as ToolMethod<unknown> casts around the actual type instead of narrowing it. Consider whether ToolMethod can be inferred/typed directly on changelogsGet to avoid the cast, per TypeScript guidance on type safety.

As per coding guidelines, "When working with TypeScript code, follow /projects/site/src/docs/internal/guidelines/typescript.md for type safety, type guards, discriminated unions, and exhaustive checking."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@projects/internals/tools/src/packages/service.test.ts` at line 99, The
`PackagesService.changelogsGet as ToolMethod<unknown>` cast is bypassing
TypeScript’s type safety in the test. Update the test to use the actual
`PackagesService.changelogsGet` type directly, or adjust the `ToolMethod` typing
so `inputSchema` can be accessed without an unsafe cast. Keep the change
localized around `changelogsGet` and `metadata.inputSchema`, and prefer
inference or proper narrowing over asserting to `ToolMethod<unknown>`.

Source: Coding guidelines

const schemaSet = new Set(schema?.properties?.name.enum ?? []);
Comment on lines +99 to +100

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd tsconfig.json projects/internals/tools -x cat {}
fd tsconfig.json -x cat {} \; 2>/dev/null | rg -n 'strict'

Repository: NVIDIA/elements

Length of output: 2036


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== service.test.ts excerpt ==\n'
cat -n projects/internals/tools/src/packages/service.test.ts | sed -n '80,115p'

printf '\n== ToolMethod definition ==\n'
rg -n "type ToolMethod|interface ToolMethod|export .*ToolMethod" projects/internals/tools/src -g '*.ts' -A 8 -B 4

printf '\n== metadata / inputSchema / properties usage ==\n'
rg -n "\.metadata(\?|)\.inputSchema|properties\?\.name\.enum|properties\?\.name\?\.enum|name\.enum" projects/internals/tools/src -g '*.ts' -A 2 -B 2

Repository: NVIDIA/elements

Length of output: 16152


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== ToolMetadata / Schema definitions ==\n'
cat -n projects/internals/tools/src/internal/tools.ts | sed -n '1,120p'

printf '\n== packages service schema setup ==\n'
rg -n "changelogsGet|inputSchema|properties|enum" projects/internals/tools/src/packages -g '*.ts' -A 4 -B 4

Repository: NVIDIA/elements

Length of output: 13579


Guard the optional metadata chain here projects/internals/tools/src/packages/service.test.ts:99-100

ToolMethod.metadata is optional, so .metadata.inputSchema can trip strict null checks, and schema?.properties?.name.enum still leaves name unguarded. Use optional chaining on both access paths.

🛡️ Proposed fix for consistent optional chaining
-    const schema = (PackagesService.changelogsGet as ToolMethod<unknown>).metadata.inputSchema;
-    const schemaSet = new Set(schema?.properties?.name.enum ?? []);
+    const schema = (PackagesService.changelogsGet as ToolMethod<unknown>).metadata?.inputSchema;
+    const schemaSet = new Set(schema?.properties?.name?.enum ?? []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const schema = (PackagesService.changelogsGet as ToolMethod<unknown>).metadata.inputSchema;
const schemaSet = new Set(schema?.properties?.name.enum ?? []);
const schema = (PackagesService.changelogsGet as ToolMethod<unknown>).metadata?.inputSchema;
const schemaSet = new Set(schema?.properties?.name?.enum ?? []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@projects/internals/tools/src/packages/service.test.ts` around lines 99 - 100,
The `PackagesService.changelogsGet` metadata access in `service.test.ts` is not
fully guarded for optional values. Update the `ToolMethod.metadata` lookup so
`inputSchema` is accessed only through optional chaining, and also guard the
`name` property before reading `enum` when building `schemaSet`. Use the
existing `PackagesService.changelogsGet` and `schema`/`schemaSet` test setup to
keep the change localized and compatible with strict null checks.

const error = await PackagesService.changelogsGet({ name: 'does-not-exist', format: 'markdown' }).catch(
e => e as Error
);
const available = error.message.split('Available packages:')[1] ?? '';
const errSet = new Set(Array.from(available.matchAll(/"([^"]+)"/g), m => m[1]));

expect(listSet.size).toBeGreaterThan(0);
expect(errSet).toEqual(listSet);
expect(schemaSet.size).toBeGreaterThan(0);
expect(errSet).toEqual(schemaSet);
});

it('should provide versions method', async () => {
const result = await PackagesService.versions();
expect(result).toBeDefined();
expect(typeof result).toBe('object');
expect(result['@nvidia-elements/core']).toBe('1.0.0');
expect(result['@nvidia-elements/core']).toMatch(/^\d+\.\d+\.\d+$/);
});
});
1 change: 1 addition & 0 deletions projects/lint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export default [
| `@nvidia-elements/lint/no-unknown-css-variable` | Disallow use of unknown --nve-* CSS theme variables. | CSS | `error` |
| `@nvidia-elements/lint/no-unknown-tags` | Disallow use of unknown nve-* tags. | HTML | `error` |
| `@nvidia-elements/lint/no-unstyled-typography` | Require typography elements to have nve-text styling applied. | HTML | `error` |
| `@nvidia-elements/lint/prefer-aria-label-in-compact-containers` | Prefer aria-label on form controls inside toolbars and page headers. | HTML | `error` |

## Links

Expand Down
5 changes: 4 additions & 1 deletion projects/lint/src/eslint/configs/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import noRestrictedPageSizing from '../rules/no-restricted-page-sizing.js';
import noNestedContainerTypes from '../rules/no-nested-container-types.js';
import noUnstyledTypography from '../rules/no-unstyled-typography.js';
import noTailwindClasses from '../rules/no-tailwind-classes.js';
import preferAriaLabelInCompactContainers from '../rules/prefer-aria-label-in-compact-containers.js';

const source = ['src/**/*.html', 'src/**/*.js', 'src/**/*.ts', 'src/**/*.tsx'];

Expand Down Expand Up @@ -90,7 +91,8 @@ export const elementsHtmlConfig: Linter.Config = {
'no-unknown-css-variable': noUnknownCssVariable,
'no-nested-container-types': noNestedContainerTypes,
'no-unstyled-typography': noUnstyledTypography,
'no-tailwind-classes': noTailwindClasses
'no-tailwind-classes': noTailwindClasses,
'prefer-aria-label-in-compact-containers': preferAriaLabelInCompactContainers
}
}
},
Expand Down Expand Up @@ -123,6 +125,7 @@ export const elementsHtmlConfig: Linter.Config = {
'@nvidia-elements/lint/no-nested-container-types': ['error'],
'@nvidia-elements/lint/no-unstyled-typography': ['error'],
'@nvidia-elements/lint/no-tailwind-classes': ['error'],
'@nvidia-elements/lint/prefer-aria-label-in-compact-containers': ['error'],
'@nvidia-elements/lint/no-unexpected-style-customization': ['off'],
'@nvidia-elements/lint/no-missing-gap-space': ['off']
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { beforeEach, describe, expect, it } from 'vitest';
import { RuleTester } from 'eslint';
import type { JSRuleDefinition } from 'eslint';
import htmlParser from '@html-eslint/parser';
import { elementsHtmlConfig } from '../configs/html.js';
import preferAriaLabelInCompactContainers from './prefer-aria-label-in-compact-containers.js';

const rule = preferAriaLabelInCompactContainers as unknown as JSRuleDefinition;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Double unknown cast bypasses type checking for the rule shape.

preferAriaLabelInCompactContainers as unknown as JSRuleDefinition sidesteps structural type checking between the custom rule object and ESLint's expected rule type, so a future incompatible change to the rule shape wouldn't be caught here.

As per coding guidelines, **/*.ts files should follow /projects/site/src/docs/internal/guidelines/typescript.md for type safety and type guards.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@projects/lint/src/eslint/rules/prefer-aria-label-in-compact-containers.test.ts`
at line 11, The `preferAriaLabelInCompactContainers` test setup is using a
double cast to `JSRuleDefinition`, which bypasses type safety and hides
incompatible rule-shape changes. Update the assertion in the test to rely on
proper structural typing or an explicit type guard instead of `as unknown as`,
and keep the check aligned with the actual rule object shape used by
`preferAriaLabelInCompactContainers`.

Source: Coding guidelines


function compactLabelError(control: string, container: string) {
return {
messageId: 'prefer-aria-label' as const,
data: { control, container }
};
}

describe('preferAriaLabelInCompactContainers', () => {
let tester: RuleTester;

beforeEach(() => {
tester = new RuleTester({
languageOptions: {
parser: htmlParser,
parserOptions: {
frontmatter: true
}
}
});
});

it('should define rule metadata', () => {
expect(preferAriaLabelInCompactContainers.meta).toBeDefined();
expect(preferAriaLabelInCompactContainers.meta.type).toBe('problem');
expect(preferAriaLabelInCompactContainers.meta.docs).toBeDefined();
expect(preferAriaLabelInCompactContainers.meta.docs.description).toBe(
'Prefer aria-label on form controls inside toolbars and page headers.'
);
expect(preferAriaLabelInCompactContainers.meta.docs.category).toBe('Best Practice');
expect(preferAriaLabelInCompactContainers.meta.docs.recommended).toBe(true);
expect(preferAriaLabelInCompactContainers.meta.docs.url).toContain('/docs/lint/');
expect(preferAriaLabelInCompactContainers.meta.schema).toEqual([]);
expect(preferAriaLabelInCompactContainers.meta.messages['prefer-aria-label']).toBe(
'Remove <label> from <{{control}}> inside <{{container}}> and use aria-label instead to preserve the compact layout.'
);
});

it('should register the rule as a recommended error', () => {
const plugin = elementsHtmlConfig.plugins?.['@nvidia-elements/lint'];

expect(plugin?.rules?.['prefer-aria-label-in-compact-containers']).toBe(preferAriaLabelInCompactContainers);
expect(elementsHtmlConfig.rules?.['@nvidia-elements/lint/prefer-aria-label-in-compact-containers']).toEqual([
'error'
]);
});

it('should allow labels that do not affect compact form controls', () => {
tester.run('allowed labels', rule, {
valid: [
`<nve-input><label>Name</label><input /></nve-input>`,
`<nve-toolbar><label nve-text="body sm">1 of 13</label></nve-toolbar>`,
`<nve-page-header><nve-button><label>Action</label></nve-button></nve-page-header>`,
`<nve-toolbar><div><label>Caption</label></div></nve-toolbar>`
],
invalid: []
});
});

it('should allow compact form controls without visual labels', () => {
tester.run('aria labels', rule, {
valid: [
`<nve-toolbar><nve-input><input aria-label="Search" /></nve-input></nve-toolbar>`,
`<nve-page-header><nve-select><select aria-label="Page"><option>One</option></select></nve-select></nve-page-header>`,
`<nve-toolbar><nve-input><input /></nve-input></nve-toolbar>`
],
invalid: []
});
});

it('should report labels in compact form controls', () => {
tester.run('compact control labels', rule, {
valid: [],
invalid: [
{
code: `<nve-toolbar><nve-input><label>Search</label><input /></nve-input></nve-toolbar>`,
errors: [compactLabelError('nve-input', 'nve-toolbar')]
},
{
code: `<nve-page-header><nve-search slot="suffix"><label>Search</label><input type="search" /></nve-search></nve-page-header>`,
errors: [compactLabelError('nve-search', 'nve-page-header')]
},
{
code: `<nve-toolbar><div><nve-select><label>Page</label><select></select></nve-select></div></nve-toolbar>`,
errors: [compactLabelError('nve-select', 'nve-toolbar')]
},
{
code: `<nve-page-header><nve-star-rating><span><label>Rating</label></span><input type="range" /></nve-star-rating></nve-page-header>`,
errors: [compactLabelError('nve-star-rating', 'nve-page-header')]
},
{
code: `<nve-toolbar><nve-input><label>Name</label><input aria-label="Name" /></nve-input></nve-toolbar>`,
errors: [compactLabelError('nve-input', 'nve-toolbar')]
}
]
});
});

it('should report each label against its nearest form control', () => {
tester.run('nested form controls', rule, {
valid: [],
invalid: [
{
code: `<nve-toolbar>
<nve-checkbox-group>
<label>Options</label>
<nve-checkbox><label>First</label><input type="checkbox" /></nve-checkbox>
</nve-checkbox-group>
</nve-toolbar>`,
errors: [
compactLabelError('nve-checkbox-group', 'nve-toolbar'),
compactLabelError('nve-checkbox', 'nve-toolbar')
]
}
]
});
});
});
Loading