fix: Restore iPadOS 15 compatibility by removing unsupported regex (#2756)

## Summary

Removes an unsupported negative regex lookbehind from input-helper
entity ID validation.

Advanced Camera Card v8 fails to load on Safari/iPadOS 15 because the
browser cannot parse `(?<!_)`. This prevents the `advanced-camera-card`
custom element from registering.

The replacement preserves the existing validation behavior without
lookbehind:

- Accepts valid `input_*` helper entity IDs.
- Rejects object IDs beginning or ending with `_`.
- Rejects object IDs containing `__`.
- Restores compatibility with Safari versions before 16.4.

This closes #2753 

## Testing

- Added regression coverage for valid and invalid input-helper entity
IDs.
- TypeScript typecheck passes.
- ESLint passes for the changed files.
- Build succeeds.
- Confirmed the generated distribution contains no negative lookbehind
syntax.
- Direct validation of the replacement regex passes.

## Reproduction

- Device: iPad mini 4
- OS: iPadOS 15.8.8
- Last working release: v7.27.4
- Affected releases: v8.0.0 and v8.0.1
- Browser error:

```text
SyntaxError: Invalid regular expression: invalid group specifier name
Safari and Safari on iOS did not support regex lookbehind until version 16.4.

---------

Co-authored-by: dermotduffy <dermot.duffy@gmail.com>
This commit is contained in:
Shane Sewell
2026-09-01 16:53:34 -07:00
committed by GitHub
co-authored by dermotduffy
parent 95b85bf608
commit 54e261fd54
2 changed files with 33 additions and 2 deletions
@@ -7,10 +7,13 @@ import type { ConditionEvaluator, ConditionOfType, EvaluatorContext } from './ty
// Home Assistant resolves an expected value that names an `input_*` helper to // Home Assistant resolves an expected value that names an `input_*` helper to
// that helper's current state (its Lovelace state-condition behavior), on both // that helper's current state (its Lovelace state-condition behavior), on both
// the state and the attribute path; only these helper domains are resolved. // the state and the attribute path; only these helper domains are resolved.
// Regexp directly from: // Matches the same entity IDs as the regexp in Home Assistant:
// https://github.com/home-assistant/core/blob/dev/homeassistant/helpers/condition.py // https://github.com/home-assistant/core/blob/dev/homeassistant/helpers/condition.py
// That regexp rejects a trailing `_` with a lookbehind, which Safari before
// 16.4 fails to parse (taking the whole card down), so the leading and trailing
// character are spelled out here instead.
const INPUT_ENTITY_ID = const INPUT_ENTITY_ID =
/^input_(?:select|text|number|boolean|datetime)\.(?!.+__)(?!_)[\da-z_]+(?<!_)$/; /^input_(?:select|text|number|boolean|datetime)\.(?!.+__)[\da-z](?:[\da-z_]*[\da-z])?$/;
const isInputHelperName = (value: unknown): value is string => const isInputHelperName = (value: unknown): value is string =>
typeof value === 'string' && INPUT_ENTITY_ID.test(value); typeof value === 'string' && INPUT_ENTITY_ID.test(value);
@@ -734,6 +734,34 @@ describe('state condition', () => {
).toBeFalsy(); ).toBeFalsy();
}); });
it.each([
['input_text.expected', true],
['input_text.a_1', true],
['input_text.', false],
['input_text._expected', false],
['input_text.__expected', false],
['input_text.expected_', false],
['input_text.expected__name', false],
])('should resolve only a valid input helper ID: %s', (entityID, shouldResolve) => {
const evaluator = createConditionEvaluator(
{
condition: 'state' as const,
entity_id: 'binary_sensor.foo',
state: entityID,
},
createEvaluatorContext(),
);
expect(
evaluator.evaluate({
hass: createHASS({
'binary_sensor.foo': createStateEntity({ state: 'armed' }),
[entityID]: createStateEntity({ state: 'armed' }),
}),
}).result,
).toBe(shouldResolve);
});
it('should not resolve a non-input entity name', () => { it('should not resolve a non-input entity name', () => {
// Only `input_*` helpers are resolved; other entity names compare literally. // Only `input_*` helpers are resolved; other entity names compare literally.
const evaluator = createConditionEvaluator( const evaluator = createConditionEvaluator(