diff --git a/projects/core/eslint-suppressions.json b/projects/core/eslint-suppressions.json
index fa9bb4a206..bf013b95c3 100644
--- a/projects/core/eslint-suppressions.json
+++ b/projects/core/eslint-suppressions.json
@@ -11,7 +11,7 @@
},
"src/format-datetime/format-datetime.test.ts": {
"@typescript-eslint/no-unnecessary-type-assertion": {
- "count": 27
+ "count": 26
}
},
"src/format-number/format-number.test.ts": {
@@ -21,7 +21,7 @@
},
"src/format-relative-time/format-relative-time.test.ts": {
"@typescript-eslint/no-unnecessary-type-assertion": {
- "count": 40
+ "count": 39
}
},
"src/internal/controllers/audit.controller.ts": {
diff --git a/projects/core/src/button-group/button-group.ts b/projects/core/src/button-group/button-group.ts
index 1d874beb97..b521f0bb8d 100644
--- a/projects/core/src/button-group/button-group.ts
+++ b/projects/core/src/button-group/button-group.ts
@@ -25,7 +25,7 @@ import globalStyles from './button-group.global.css?inline';
* @documentation https://nvidia.github.io/elements/docs/elements/button-group/
* @since 0.16.0
* @entrypoint \@nvidia-elements/core/button-group
- * @slot - default slot for `nve-button` or `nve-icon-button`
+ * @slot - default slot for `nve-button`, `nve-icon-button`, `nve-divider`, `nve-media-seek-button`, or `nve-media-pause-button`
* @cssprop --background
* @cssprop --border-radius
* @cssprop --padding
@@ -66,7 +66,7 @@ export class ButtonGroup extends LitElement {
static readonly metadata = {
tag: 'nve-button-group',
version: '0.0.0',
- children: ['nve-button', 'nve-icon-button', 'nve-divider']
+ children: ['nve-button', 'nve-icon-button', 'nve-divider', 'nve-media-seek-button', 'nve-media-pause-button']
};
/** @private */
diff --git a/projects/core/src/card/card.ts b/projects/core/src/card/card.ts
index 39310ba3a6..ac3b119e5f 100644
--- a/projects/core/src/card/card.ts
+++ b/projects/core/src/card/card.ts
@@ -62,7 +62,6 @@ export class Card extends LitElement implements ContainerElement {
* @cssprop --gap
* @aria https://github.com/w3c/aria-practices/issues
*/
-@audit()
export class CardHeader extends LitElement {
static styles = useStyles([cardHeaderStyleSheet]);
@@ -96,7 +95,6 @@ export class CardHeader extends LitElement {
* @cssprop --padding
* @aria https://github.com/w3c/aria-practices/issues
*/
-@audit()
export class CardContent extends LitElement {
static styles = useStyles([cardContentStyleSheet]);
@@ -124,7 +122,6 @@ export class CardContent extends LitElement {
* @cssprop --border-top
* @aria https://github.com/w3c/aria-practices/issues
*/
-@audit()
export class CardFooter extends LitElement {
static styles = useStyles([cardFooterStyleSheet]);
diff --git a/projects/core/src/dialog/dialog-footer.ts b/projects/core/src/dialog/dialog-footer.ts
index bd2197fc49..041df46510 100644
--- a/projects/core/src/dialog/dialog-footer.ts
+++ b/projects/core/src/dialog/dialog-footer.ts
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import { html, LitElement } from 'lit';
-import { audit, useStyles } from '@nvidia-elements/core/internal';
+import { useStyles } from '@nvidia-elements/core/internal';
import styles from './dialog-footer.css?inline';
/**
@@ -17,7 +17,6 @@ import styles from './dialog-footer.css?inline';
* @cssprop --gap
* @aria https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
*/
-@audit()
export class DialogFooter extends LitElement {
static styles = useStyles([styles]);
diff --git a/projects/core/src/dialog/dialog-header.ts b/projects/core/src/dialog/dialog-header.ts
index 655e4a21e4..d2e503451c 100644
--- a/projects/core/src/dialog/dialog-header.ts
+++ b/projects/core/src/dialog/dialog-header.ts
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import { html, LitElement } from 'lit';
-import { audit, useStyles } from '@nvidia-elements/core/internal';
+import { useStyles } from '@nvidia-elements/core/internal';
import styles from './dialog-header.css?inline';
/**
@@ -16,7 +16,6 @@ import styles from './dialog-header.css?inline';
* @cssprop --padding
* @aria https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
*/
-@audit()
export class DialogHeader extends LitElement {
static styles = useStyles([styles]);
diff --git a/projects/core/src/drawer/drawer-content.ts b/projects/core/src/drawer/drawer-content.ts
index 5ef196130e..6786f11e21 100644
--- a/projects/core/src/drawer/drawer-content.ts
+++ b/projects/core/src/drawer/drawer-content.ts
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import { html, LitElement } from 'lit';
-import { audit, useStyles } from '@nvidia-elements/core/internal';
+import { useStyles } from '@nvidia-elements/core/internal';
import styles from './drawer-content.css?inline';
/**
@@ -16,7 +16,6 @@ import styles from './drawer-content.css?inline';
* @cssprop --width
* @aria https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
*/
-@audit()
export class DrawerContent extends LitElement {
static styles = useStyles([styles]);
diff --git a/projects/core/src/drawer/drawer-footer.ts b/projects/core/src/drawer/drawer-footer.ts
index 4050d11198..73a49006c4 100644
--- a/projects/core/src/drawer/drawer-footer.ts
+++ b/projects/core/src/drawer/drawer-footer.ts
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import { html, LitElement } from 'lit';
-import { audit, useStyles } from '@nvidia-elements/core/internal';
+import { useStyles } from '@nvidia-elements/core/internal';
import styles from './drawer-footer.css?inline';
/**
@@ -18,7 +18,6 @@ import styles from './drawer-footer.css?inline';
* @cssprop --min-height
* @aria https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
*/
-@audit()
export class DrawerFooter extends LitElement {
static styles = useStyles([styles]);
diff --git a/projects/core/src/drawer/drawer-header.ts b/projects/core/src/drawer/drawer-header.ts
index f3e259b66d..9fedf619b9 100644
--- a/projects/core/src/drawer/drawer-header.ts
+++ b/projects/core/src/drawer/drawer-header.ts
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import { html, LitElement } from 'lit';
-import { audit, useStyles } from '@nvidia-elements/core/internal';
+import { useStyles } from '@nvidia-elements/core/internal';
import styles from './drawer-header.css?inline';
/**
@@ -17,7 +17,6 @@ import styles from './drawer-header.css?inline';
* @cssprop --padding
* @aria https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
*/
-@audit()
export class DrawerHeader extends LitElement {
static styles = useStyles([styles]);
diff --git a/projects/core/src/dropdown/dropdown-footer.ts b/projects/core/src/dropdown/dropdown-footer.ts
index 90fcda8794..58c9afee09 100644
--- a/projects/core/src/dropdown/dropdown-footer.ts
+++ b/projects/core/src/dropdown/dropdown-footer.ts
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import { html, LitElement } from 'lit';
-import { audit, useStyles } from '@nvidia-elements/core/internal';
+import { useStyles } from '@nvidia-elements/core/internal';
import styles from './dropdown-footer.css?inline';
/**
@@ -17,7 +17,6 @@ import styles from './dropdown-footer.css?inline';
* @cssprop --gap
* @aria https://www.w3.org/WAI/ARIA/apg/patterns/menubar/
*/
-@audit()
export class DropdownFooter extends LitElement {
static styles = useStyles([styles]);
diff --git a/projects/core/src/dropdown/dropdown-header.ts b/projects/core/src/dropdown/dropdown-header.ts
index c267282e13..56286546be 100644
--- a/projects/core/src/dropdown/dropdown-header.ts
+++ b/projects/core/src/dropdown/dropdown-header.ts
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import { html, LitElement } from 'lit';
-import { audit, useStyles } from '@nvidia-elements/core/internal';
+import { useStyles } from '@nvidia-elements/core/internal';
import styles from './dropdown-header.css?inline';
/**
@@ -16,7 +16,6 @@ import styles from './dropdown-header.css?inline';
* @cssprop --padding
* @aria https://www.w3.org/WAI/ARIA/apg/patterns/menubar/
*/
-@audit()
export class DropdownHeader extends LitElement {
static styles = useStyles([styles]);
diff --git a/projects/core/src/format-bytes/format-bytes.test.ts b/projects/core/src/format-bytes/format-bytes.test.ts
index c3d20814a2..a51a4fc6f3 100644
--- a/projects/core/src/format-bytes/format-bytes.test.ts
+++ b/projects/core/src/format-bytes/format-bytes.test.ts
@@ -228,51 +228,81 @@ describe(FormatBytes.metadata.tag, () => {
['display', 'invalid'],
['unit', 'invalid'],
['unit-display', 'invalid']
- ])('should preserve input for an invalid %s option', async (attribute, value) => {
+ ])('should preserve input without warning for an invalid %s option', async (attribute, value) => {
const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined);
element.setAttribute(attribute, value);
await elementIsStable(element);
expect(renderedText(element)).toBe('1048576');
- expect(warn).toHaveBeenCalledOnce();
+ expect(warn).not.toHaveBeenCalled();
});
- it('should preserve and warn for invalid numeric input', async () => {
- const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined);
- element.textContent = 'not-a-number';
- await elementIsStable(element);
+ it.each(['not-a-number', 'NaN', 'Infinity', '-Infinity', '1e309'])(
+ 'should preserve invalid numeric input %s without warning',
+ async value => {
+ const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined);
+ element.textContent = value;
+ await elementIsStable(element);
- expect(renderedText(element)).toBe('not-a-number');
- expect(warn).toHaveBeenCalledWith('format-bytes: invalid numeric value "not-a-number"');
- });
+ expect(renderedText(element)).toBe(value);
+ expect(warn).not.toHaveBeenCalled();
+ }
+ );
- it('should preserve and warn for an invalid value attribute', async () => {
+ it('should preserve an invalid value attribute without warning', async () => {
const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined);
element.setAttribute('value', 'not-a-number');
await elementIsStable(element);
expect(renderedData(element)?.getAttribute('value')).toBe('not-a-number');
expect(renderedText(element)).toBe('not-a-number');
- expect(warn).toHaveBeenCalledWith('format-bytes: invalid numeric value "not-a-number"');
+ expect(warn).not.toHaveBeenCalled();
});
- it('should preserve and warn for a NaN value property', async () => {
+ it('should preserve a NaN value property without warning', async () => {
const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined);
element.value = Number.NaN;
await elementIsStable(element);
expect(renderedData(element)?.getAttribute('value')).toBe('NaN');
expect(renderedText(element)).toBe('NaN');
- expect(warn).toHaveBeenCalledWith('format-bytes: invalid numeric value "NaN"');
+ expect(warn).not.toHaveBeenCalled();
});
- it('should preserve input for invalid fraction digit options', async () => {
+ it('should preserve input silently for invalid fraction digit options', async () => {
const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined);
element.minimumFractionDigits = 3;
element.maximumFractionDigits = 2;
await elementIsStable(element);
expect(renderedText(element)).toBe('1048576');
- expect(warn).toHaveBeenCalledOnce();
+ expect(warn).not.toHaveBeenCalled();
+ });
+
+ it.each([
+ ['locale', 'en_US'],
+ ['minimum-fraction-digits', '-1'],
+ ['maximum-fraction-digits', '101'],
+ ['maximum-fraction-digits', 'NaN'],
+ ['maximum-fraction-digits', 'Infinity']
+ ])('should preserve input without warning for invalid %s', async (attribute, value) => {
+ const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined);
+ element.setAttribute(attribute, value);
+ await elementIsStable(element);
+
+ expect(renderedText(element)).toBe('1048576');
+ expect(warn).not.toHaveBeenCalled();
+ });
+
+ it('should resume formatting after correcting an invalid locale', async () => {
+ const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined);
+ element.locale = 'en_US';
+ await elementIsStable(element);
+ expect(renderedText(element)).toBe('1048576');
+
+ element.locale = 'en-US';
+ await elementIsStable(element);
+ expect(renderedText(element)).toBe('1.05 MB');
+ expect(warn).not.toHaveBeenCalled();
});
});
diff --git a/projects/core/src/format-bytes/format-bytes.ts b/projects/core/src/format-bytes/format-bytes.ts
index 344cf8e8a8..85feaa291a 100644
--- a/projects/core/src/format-bytes/format-bytes.ts
+++ b/projects/core/src/format-bytes/format-bytes.ts
@@ -3,7 +3,7 @@
import { html, LitElement } from 'lit';
import { property } from 'lit/decorators/property.js';
-import { LogService, typeSSR, useStyles } from '@nvidia-elements/core/internal';
+import { typeSSR, useStyles } from '@nvidia-elements/core/internal';
import styles from './format-bytes.css?inline';
export type FormatBytesDisplay = 'decimal' | 'binary';
@@ -122,7 +122,6 @@ export class FormatBytes extends LitElement {
const numericValue = Number(rawValue);
if (Number.isFinite(numericValue)) return numericValue;
- LogService.warn(`format-bytes: invalid numeric value "${rawValue}"`);
return null;
}
@@ -144,24 +143,8 @@ export class FormatBytes extends LitElement {
return Math.abs(convertedValue) === 1 ? labels.singular : labels.plural;
}
- #warnInvalidOption(name: string, value: unknown): void {
- LogService.warn(`format-bytes: invalid ${name} value "${String(value)}"`);
- }
-
#hasValidConfiguration(): boolean {
- if (!isDisplay(this.display)) {
- this.#warnInvalidOption('display', this.display);
- return false;
- }
- if (!isUnitDisplay(this.unitDisplay)) {
- this.#warnInvalidOption('unit-display', this.unitDisplay);
- return false;
- }
- if (this.unit !== undefined && !isUnit(this.unit)) {
- this.#warnInvalidOption('unit', this.unit);
- return false;
- }
- return true;
+ return isDisplay(this.display) && isUnitDisplay(this.unitDisplay) && (this.unit === undefined || isUnit(this.unit));
}
get #numberFormatOptions(): Intl.NumberFormatOptions {
@@ -201,9 +184,7 @@ export class FormatBytes extends LitElement {
try {
const roundedValue = this.#roundNumber(convertedValue);
return `${this.#formatNumber(roundedValue)} ${this.#formatLabel(resolvedUnit, roundedValue)}`;
- } catch (error) {
- const message = error instanceof Error ? error.message : String(error);
- LogService.warn(`format-bytes: ${message}`);
+ } catch {
return rawValue;
}
}
diff --git a/projects/core/src/format-datetime/format-datetime.test.ts b/projects/core/src/format-datetime/format-datetime.test.ts
index 21ef10e4a5..bb66afeff8 100644
--- a/projects/core/src/format-datetime/format-datetime.test.ts
+++ b/projects/core/src/format-datetime/format-datetime.test.ts
@@ -2,9 +2,10 @@
// SPDX-License-Identifier: Apache-2.0
import { html } from 'lit';
-import { describe, expect, it, beforeEach, afterEach } from 'vitest';
+import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest';
import { createFixture, elementIsStable, removeFixture } from '@internals/testing';
import { FormatDatetime } from '@nvidia-elements/core/format-datetime';
+import { LogService } from '@nvidia-elements/core/internal';
import '@nvidia-elements/core/format-datetime/define.js';
describe(FormatDatetime.metadata.tag, () => {
@@ -24,6 +25,7 @@ describe(FormatDatetime.metadata.tag, () => {
afterEach(() => {
document.documentElement.lang = originalDocumentLang;
removeFixture(fixture);
+ vi.restoreAllMocks();
});
it('should define element', () => {
@@ -315,18 +317,93 @@ describe(FormatDatetime.metadata.tag, () => {
expect(time!.textContent!.trim()).toBe('7/28/23');
});
- it('should show raw string for invalid date', async () => {
- element.date = 'not-a-date';
+ it.each(['not-a-date', '2026-13-01', '2026-10-26T25:00:00Z', '+999999-01-01'])(
+ 'should preserve invalid date %s without warning',
+ async value => {
+ const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined);
+ element.date = value;
+ await elementIsStable(element);
+
+ const time = element.shadowRoot!.querySelector('time');
+ expect(time?.getAttribute('datetime')).toBe(value);
+ expect(time?.textContent.trim()).toBe(value);
+ expect(warn).not.toHaveBeenCalled();
+ }
+ );
+
+ it('should preserve invalid date attributes without warning', async () => {
+ const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined);
+ element.setAttribute('date', 'not-a-date');
await elementIsStable(element);
+
const time = element.shadowRoot!.querySelector('time');
- expect(time!.textContent!.trim()).toBe('not-a-date');
+ expect(time?.getAttribute('datetime')).toBe('not-a-date');
+ expect(time?.textContent.trim()).toBe('not-a-date');
+ expect(warn).not.toHaveBeenCalled();
});
- it('should fall back to raw string for invalid Intl options', async () => {
+ it('should preserve invalid text content without warning', async () => {
+ const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined);
+ element.textContent = 'not-a-date';
+ await elementIsStable(element);
+
+ const time = element.shadowRoot!.querySelector('time');
+ expect(time?.getAttribute('datetime')).toBe('not-a-date');
+ expect(time?.textContent.trim()).toBe('not-a-date');
+ expect(warn).not.toHaveBeenCalled();
+ });
+
+ it('should fall back silently to raw string for invalid Intl options', async () => {
+ const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined);
element.setAttribute('date-style', 'banana');
await elementIsStable(element);
const time = element.shadowRoot!.querySelector('time');
expect(time!.textContent!.trim()).toBe('2023-07-28T04:20:17.434Z');
+ expect(warn).not.toHaveBeenCalled();
+ });
+
+ it.each([
+ ['locale', 'en_US'],
+ ['locale', ''],
+ ['time-zone', 'Invalid/Zone'],
+ ['time-zone', '+24:00'],
+ ['time-zone', ' ']
+ ])('should preserve input without warning for invalid %s=%s', async (attribute, value) => {
+ const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined);
+ element.setAttribute(attribute, value);
+ await elementIsStable(element);
+
+ const time = element.shadowRoot?.querySelector('time');
+ expect(time?.textContent.trim()).toBe('2023-07-28T04:20:17.434Z');
+ expect(time?.getAttribute('datetime')).toBe('2023-07-28T04:20:17.434Z');
+ expect(warn).not.toHaveBeenCalled();
+ });
+
+ it.each([
+ ['locale', 'en_US', 'en-US'],
+ ['time-zone', 'Invalid/Zone', 'UTC']
+ ])('should resume formatting after correcting %s', async (attribute, invalid, valid) => {
+ const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined);
+ element.setAttribute(attribute, invalid);
+ await elementIsStable(element);
+ expect(element.shadowRoot?.querySelector('time')?.textContent.trim()).toBe('2023-07-28T04:20:17.434Z');
+
+ element.setAttribute(attribute, valid);
+ await elementIsStable(element);
+ expect(element.shadowRoot?.querySelector('time')?.textContent.trim()).toBe('July 28, 2023');
+ expect(warn).not.toHaveBeenCalled();
+ });
+
+ it('should use the default time zone for an empty time zone option', async () => {
+ const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined);
+ element.timeZone = '';
+ await elementIsStable(element);
+ const expected = new Intl.DateTimeFormat('en-US', { dateStyle: 'long' }).format(
+ new Date('2023-07-28T04:20:17.434Z')
+ );
+
+ expect(element.shadowRoot?.querySelector('time')?.textContent.trim()).toBe(expected);
+ expect(warn).not.toHaveBeenCalled();
});
it('should render empty for no content', async () => {
diff --git a/projects/core/src/format-datetime/format-datetime.ts b/projects/core/src/format-datetime/format-datetime.ts
index 4795ad826f..a89412464c 100644
--- a/projects/core/src/format-datetime/format-datetime.ts
+++ b/projects/core/src/format-datetime/format-datetime.ts
@@ -3,7 +3,7 @@
import { html, LitElement } from 'lit';
import { property } from 'lit/decorators/property.js';
-import { useStyles, typeSSR, LogService } from '@nvidia-elements/core/internal';
+import { useStyles, typeSSR } from '@nvidia-elements/core/internal';
import styles from './format-datetime.css?inline';
/**
@@ -111,8 +111,7 @@ export class FormatDatetime extends LitElement {
try {
return new Intl.DateTimeFormat(this.#resolvedLocale, this.#formatOptions).format(date);
- } catch (e) {
- LogService.warn(`format-datetime: ${(e as Error).message}`);
+ } catch {
return iso;
}
}
@@ -155,7 +154,6 @@ export class FormatDatetime extends LitElement {
const date = new Date(iso);
if (!isNaN(date.getTime())) return date;
- LogService.warn(`format-datetime: invalid date value "${iso}"`);
return null;
}
diff --git a/projects/core/src/format-number/format-number.test.ts b/projects/core/src/format-number/format-number.test.ts
index 7ea24d4d22..1e3b42f93c 100644
--- a/projects/core/src/format-number/format-number.test.ts
+++ b/projects/core/src/format-number/format-number.test.ts
@@ -290,26 +290,38 @@ describe(FormatNumber.metadata.tag, () => {
warnSpy.mockRestore();
});
- it('should show raw string for invalid number input', async () => {
- element.number = 'not-a-number';
+ it.each(['not-a-number', 'NaN', 'Infinity', '-Infinity', '1e309'])(
+ 'should preserve invalid number %s without warning',
+ async value => {
+ element.number = value;
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toBe(value);
+ expect(warnSpy).not.toHaveBeenCalled();
+ }
+ );
+
+ it('should preserve invalid text content without warning', async () => {
+ element.textContent = 'not-a-number';
await elementIsStable(element);
const data = element.shadowRoot!.querySelector('data');
- expect(data!.textContent!.trim()).toBe('not-a-number');
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('invalid numeric value'));
+ expect(data!.textContent.trim()).toBe('not-a-number');
+ expect(warnSpy).not.toHaveBeenCalled();
});
- it('should fall back for invalid Intl options', async () => {
+ it('should fall back silently for invalid Intl options', async () => {
element.number = '1234.56';
element.setAttribute('format-style', 'banana');
await elementIsStable(element);
const data = element.shadowRoot!.querySelector('data');
expect(data!.textContent!.trim()).toBe('1234.56');
- expect(warnSpy).toHaveBeenCalled();
+ expect(warnSpy).not.toHaveBeenCalled();
});
- it('should fall back when currency is missing', async () => {
+ it('should fall back silently when currency is missing', async () => {
element.number = '1234.56';
element.formatStyle = 'currency';
element.currency = undefined;
@@ -317,10 +329,10 @@ describe(FormatNumber.metadata.tag, () => {
const data = element.shadowRoot!.querySelector('data');
expect(data!.textContent!.trim()).toBe('1234.56');
- expect(warnSpy).toHaveBeenCalled();
+ expect(warnSpy).not.toHaveBeenCalled();
});
- it('should fall back when unit is missing', async () => {
+ it('should fall back silently when unit is missing', async () => {
element.number = '12';
element.formatStyle = 'unit';
element.unit = undefined;
@@ -328,7 +340,46 @@ describe(FormatNumber.metadata.tag, () => {
const data = element.shadowRoot!.querySelector('data');
expect(data!.textContent!.trim()).toBe('12');
- expect(warnSpy).toHaveBeenCalled();
+ expect(warnSpy).not.toHaveBeenCalled();
+ });
+
+ it.each([
+ ['locale', 'en_US'],
+ ['currency', 'US'],
+ ['unit', 'invalid'],
+ ['minimum-fraction-digits', '-1'],
+ ['maximum-fraction-digits', '101'],
+ ['maximum-fraction-digits', 'NaN'],
+ ['minimum-integer-digits', '0']
+ ])('should preserve input without warning for invalid %s', async (attribute, value) => {
+ element.number = '1234.56';
+ element.setAttribute(attribute, value);
+ await elementIsStable(element);
+
+ expect(element.shadowRoot!.querySelector('data')!.textContent.trim()).toBe('1234.56');
+ expect(warnSpy).not.toHaveBeenCalled();
+ });
+
+ it('should preserve input silently for reversed fraction digit bounds', async () => {
+ element.number = '1234.56';
+ element.minimumFractionDigits = 3;
+ element.maximumFractionDigits = 2;
+ await elementIsStable(element);
+
+ expect(element.shadowRoot!.querySelector('data')!.textContent.trim()).toBe('1234.56');
+ expect(warnSpy).not.toHaveBeenCalled();
+ });
+
+ it('should resume formatting after correcting an invalid locale', async () => {
+ element.number = '1234.56';
+ element.locale = 'en_US';
+ await elementIsStable(element);
+ expect(element.shadowRoot!.querySelector('data')!.textContent.trim()).toBe('1234.56');
+
+ element.locale = 'en-US';
+ await elementIsStable(element);
+ expect(element.shadowRoot!.querySelector('data')!.textContent.trim()).toBe('1,234.56');
+ expect(warnSpy).not.toHaveBeenCalled();
});
it('should render empty for no content', async () => {
diff --git a/projects/core/src/format-number/format-number.ts b/projects/core/src/format-number/format-number.ts
index c28efa7a8d..77d4c00578 100644
--- a/projects/core/src/format-number/format-number.ts
+++ b/projects/core/src/format-number/format-number.ts
@@ -3,7 +3,7 @@
import { html, LitElement } from 'lit';
import { property } from 'lit/decorators/property.js';
-import { useStyles, typeSSR, LogService } from '@nvidia-elements/core/internal';
+import { useStyles, typeSSR } from '@nvidia-elements/core/internal';
import styles from './format-number.css?inline';
export type FormatNumberStyle = 'decimal' | 'currency' | 'percent' | 'unit';
@@ -114,12 +114,12 @@ export class FormatNumber extends LitElement {
useGrouping?: 'auto' | 'always' | 'min2' | 'true' | 'false' | boolean;
/**
- * Pad fraction output to at least this many digits (0-20).
+ * Pad fraction output to at least this many digits (0-100).
*/
@property({ type: Number, attribute: 'minimum-fraction-digits' }) minimumFractionDigits?: number;
/**
- * Round fraction output to at most this many digits (0-20).
+ * Round fraction output to at most this many digits (0-100).
*/
@property({ type: Number, attribute: 'maximum-fraction-digits' }) maximumFractionDigits?: number;
@@ -143,7 +143,6 @@ export class FormatNumber extends LitElement {
const numericValue = Number(raw);
if (Number.isFinite(numericValue)) return numericValue;
- LogService.warn(`format-number: invalid numeric value "${raw}"`);
return null;
}
@@ -177,8 +176,7 @@ export class FormatNumber extends LitElement {
try {
return new Intl.NumberFormat(this.#resolvedLocale, this.#formatOptions).format(numericValue);
- } catch (e) {
- LogService.warn(`format-number: ${(e as Error).message}`);
+ } catch {
return raw;
}
}
diff --git a/projects/core/src/format-relative-time/format-relative-time.test.ts b/projects/core/src/format-relative-time/format-relative-time.test.ts
index 970a616be1..47aa799f8f 100644
--- a/projects/core/src/format-relative-time/format-relative-time.test.ts
+++ b/projects/core/src/format-relative-time/format-relative-time.test.ts
@@ -5,6 +5,7 @@ import { html } from 'lit';
import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest';
import { createFixture, elementIsStable, removeFixture } from '@internals/testing';
import { FormatRelativeTime } from '@nvidia-elements/core/format-relative-time';
+import { LogService } from '@nvidia-elements/core/internal';
import '@nvidia-elements/core/format-relative-time/define.js';
const NOW = new Date('2023-07-28T12:00:00.000Z');
@@ -30,6 +31,7 @@ describe(FormatRelativeTime.metadata.tag, () => {
vi.useRealTimers();
document.documentElement.lang = originalDocumentLang;
removeFixture(fixture);
+ vi.restoreAllMocks();
});
it('should define element', () => {
@@ -384,11 +386,41 @@ describe(FormatRelativeTime.metadata.tag, () => {
});
describe('fallback', () => {
- it('should show raw string for invalid date', async () => {
- element.date = 'not-a-date';
+ it.each(['not-a-date', '2026-13-01', '2026-10-26T25:00:00Z', '+999999-01-01'])(
+ 'should preserve invalid date %s without warning',
+ async value => {
+ const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined);
+ element.date = value;
+ await elementIsStable(element);
+
+ const time = element.shadowRoot!.querySelector('time');
+ expect(time?.getAttribute('datetime')).toBe(value);
+ expect(time?.textContent.trim()).toBe(value);
+ expect(warn).not.toHaveBeenCalled();
+ }
+ );
+
+ it('should preserve invalid date attributes without warning', async () => {
+ const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined);
+ element.setAttribute('date', 'not-a-date');
+ await elementIsStable(element);
+
+ const time = element.shadowRoot!.querySelector('time');
+ expect(time?.getAttribute('datetime')).toBe('not-a-date');
+ expect(time?.textContent.trim()).toBe('not-a-date');
+ expect(warn).not.toHaveBeenCalled();
+ });
+
+ it('should preserve invalid text content without warning', async () => {
+ const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined);
+ element.date = undefined;
+ element.textContent = 'not-a-date';
await elementIsStable(element);
+
const time = element.shadowRoot!.querySelector('time');
- expect(time!.textContent!.trim()).toBe('not-a-date');
+ expect(time?.getAttribute('datetime')).toBe('not-a-date');
+ expect(time?.textContent.trim()).toBe('not-a-date');
+ expect(warn).not.toHaveBeenCalled();
});
it('should render empty for no content', async () => {
@@ -402,13 +434,55 @@ describe(FormatRelativeTime.metadata.tag, () => {
expect(time!.textContent!.trim()).toBe('');
});
- it('should fall back to raw string for invalid options', async () => {
+ it('should fall back silently to raw string for invalid options', async () => {
+ const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined);
element.setAttribute('format-style', 'banana');
await elementIsStable(element);
const time = element.shadowRoot!.querySelector('time');
expect(time!.textContent!.trim()).toBe('2023-07-27T12:00:00.000Z');
+ expect(warn).not.toHaveBeenCalled();
+ });
+
+ it.each([
+ ['locale', 'en_US'],
+ ['locale', ''],
+ ['numeric', 'invalid'],
+ ['unit', 'invalid']
+ ])('should preserve input without warning for invalid %s=%s', async (attribute, value) => {
+ const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined);
+ element.setAttribute(attribute, value);
+ await elementIsStable(element);
+
+ const time = element.shadowRoot?.querySelector('time');
+ expect(time?.textContent.trim()).toBe('2023-07-27T12:00:00.000Z');
+ expect(time?.getAttribute('datetime')).toBe('2023-07-27T12:00:00.000Z');
+ expect(warn).not.toHaveBeenCalled();
+ });
+
+ it('should resume relative formatting after correcting an invalid locale', async () => {
+ const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined);
+ element.locale = 'en_US';
+ await elementIsStable(element);
+ expect(element.shadowRoot?.querySelector('time')?.textContent.trim()).toBe('2023-07-27T12:00:00.000Z');
+
+ element.locale = 'en-US';
+ await elementIsStable(element);
+ expect(element.shadowRoot?.querySelector('time')?.textContent.trim()).toBe('1 day ago');
+ expect(warn).not.toHaveBeenCalled();
});
+ it.each(['-271821-04-20T00:00:00.000Z', '+275760-09-13T00:00:00.000Z'])(
+ 'should format the valid date boundary %s without warning',
+ async value => {
+ const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined);
+ element.date = value;
+ await elementIsStable(element);
+
+ expect(element.shadowRoot?.querySelector('time')?.textContent).toContain('years');
+ expect(warn).not.toHaveBeenCalled();
+ }
+ );
+
it('should fall back to raw string for relative values outside Intl range', async () => {
vi.useRealTimers();
const now = vi.spyOn(Date, 'now').mockReturnValue(Number.NEGATIVE_INFINITY);
diff --git a/projects/core/src/format-relative-time/format-relative-time.ts b/projects/core/src/format-relative-time/format-relative-time.ts
index 03d98b9836..c3433ecf40 100644
--- a/projects/core/src/format-relative-time/format-relative-time.ts
+++ b/projects/core/src/format-relative-time/format-relative-time.ts
@@ -3,7 +3,7 @@
import { html, LitElement } from 'lit';
import { property } from 'lit/decorators/property.js';
-import { useStyles, typeSSR, LogService } from '@nvidia-elements/core/internal';
+import { useStyles, typeSSR } from '@nvidia-elements/core/internal';
import styles from './format-relative-time.css?inline';
type TimeUnitOption = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year';
@@ -117,7 +117,6 @@ export class FormatRelativeTime extends LitElement {
const target = new Date(iso);
if (isNaN(target.getTime())) {
- LogService.warn(`format-relative-time: invalid date value "${iso}"`);
return iso;
}
@@ -128,8 +127,7 @@ export class FormatRelativeTime extends LitElement {
numeric: this.numeric,
style: this.formatStyle
}).format(value, resolvedUnit);
- } catch (e) {
- LogService.warn(`format-relative-time: ${(e as Error).message}`);
+ } catch {
return iso;
}
}
diff --git a/projects/core/src/icon/icon.test.visual.ts b/projects/core/src/icon/icon.test.visual.ts
index 64f53af30d..1a914b411c 100644
--- a/projects/core/src/icon/icon.test.visual.ts
+++ b/projects/core/src/icon/icon.test.visual.ts
@@ -7,12 +7,12 @@ import { ICON_NAMES, ICON_NAMES_SOLID } from '@nvidia-elements/core/icon';
describe('icon visual', () => {
test('icon should match visual baseline', async () => {
- const report = await visualRunner.render('icon', template(), { network: true });
+ const report = await visualRunner.render('icon', template(), { waitFor: waitForIcons });
expect(report.maxDiffPercentage).toBeLessThan(0.1);
});
test('icon should match visual baseline dark theme', async () => {
- const report = await visualRunner.render('icon.dark', template('dark'), { network: true });
+ const report = await visualRunner.render('icon.dark', template('dark'), { waitFor: waitForIcons });
expect(report.maxDiffPercentage).toBeLessThan(0.1);
});
@@ -57,7 +57,7 @@ describe('icon visual', () => {
.join('')}
`,
- { network: true }
+ { waitFor: waitForIcons }
);
expect(report.maxDiffPercentage).toBeLessThan(0.1);
});
@@ -84,7 +84,7 @@ describe('icon visual', () => {
${ICONS_WITH_OPTICAL_SPACING.map(i => ``).join('')}
`,
- { network: true }
+ { waitFor: waitForIcons }
);
expect(report.maxDiffPercentage).toBeLessThan(0.1);
});
@@ -111,12 +111,18 @@ describe('icon visual', () => {
${ICON_NAMES_SOLID.map(i => ``).join('')}
`,
- { network: true }
+ { waitFor: waitForIcons }
);
expect(report.maxDiffPercentage).toBeLessThan(0.1);
});
});
+async function waitForIcons(waitForFunction: (...args: unknown[]) => Promise) {
+ await waitForFunction(() =>
+ Array.from(document.querySelectorAll('nve-icon')).every(icon => icon.shadowRoot?.querySelector('svg'))
+ );
+}
+
function template(theme: '' | 'dark' = '') {
return /* html */ `