From 26e22d0725b124c13b738318b70700ba16dc38fb Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Fri, 12 Sep 2025 13:49:56 -0600 Subject: [PATCH 1/9] fix: isTrustedSignal/Context should return false when there is no trusted set --- packages/@lwc/shared/src/__tests__/context.spec.ts | 4 ++-- packages/@lwc/shared/src/__tests__/signals.spec.ts | 4 ++-- packages/@lwc/shared/src/context.ts | 6 ++---- packages/@lwc/shared/src/signals.ts | 5 +---- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/@lwc/shared/src/__tests__/context.spec.ts b/packages/@lwc/shared/src/__tests__/context.spec.ts index d5fdf67093..f6a08d8af6 100644 --- a/packages/@lwc/shared/src/__tests__/context.spec.ts +++ b/packages/@lwc/shared/src/__tests__/context.spec.ts @@ -96,8 +96,8 @@ describe('context', () => { expect(isTrustedContext({})).toBe(false); }); - it('should return true for all calls when trustedContexts is not set', () => { - expect(isTrustedContext({})).toBe(true); + it('should return false for all calls when trustedContexts is not set', () => { + expect(isTrustedContext({})).toBe(false); }); }); }); diff --git a/packages/@lwc/shared/src/__tests__/signals.spec.ts b/packages/@lwc/shared/src/__tests__/signals.spec.ts index b958052a79..93a2f5f72e 100644 --- a/packages/@lwc/shared/src/__tests__/signals.spec.ts +++ b/packages/@lwc/shared/src/__tests__/signals.spec.ts @@ -53,8 +53,8 @@ describe('signals', () => { expect(isTrustedSignal({})).toBe(false); }); - it('should return true for all calls when trustedSignals is not set', () => { - expect(isTrustedSignal({})).toBe(true); + it('should return false for all calls when trustedSignals is not set', () => { + expect(isTrustedSignal({})).toBe(false); }); }); }); diff --git a/packages/@lwc/shared/src/context.ts b/packages/@lwc/shared/src/context.ts index a75ac4402c..9ec70c716c 100644 --- a/packages/@lwc/shared/src/context.ts +++ b/packages/@lwc/shared/src/context.ts @@ -47,15 +47,13 @@ export function setTrustedContextSet(context: WeakSet) { } export function addTrustedContext(contextParticipant: object) { - // This should be a no-op when the trustedSignals set isn't set by runtime + // This should be a no-op when the trustedContext set isn't set by runtime trustedContext?.add(contextParticipant); } export function isTrustedContext(target: object): boolean { if (!trustedContext) { - // The runtime didn't set a trustedContext set - // this check should only be performed for runtimes that care about filtering context participants to track - return true; + return false; } return trustedContext.has(target); } diff --git a/packages/@lwc/shared/src/signals.ts b/packages/@lwc/shared/src/signals.ts index 5bfb66b27c..49f78565ac 100644 --- a/packages/@lwc/shared/src/signals.ts +++ b/packages/@lwc/shared/src/signals.ts @@ -21,10 +21,7 @@ export function addTrustedSignal(signal: object) { export function isTrustedSignal(target: object): boolean { if (!trustedSignals) { - // The runtime didn't set a trustedSignals set - // this check should only be performed for runtimes that care about filtering signals to track - // our default behavior should be to track all signals - return true; + return false; } return trustedSignals.has(target); } From 5973cc75808039d03f897b7d67a00c5122e76492 Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Mon, 15 Sep 2025 14:08:59 -0600 Subject: [PATCH 2/9] fix: add killswitch --- .../src/framework/modules/context.ts | 25 +++++++++++++++++-- .../src/framework/mutation-tracker.ts | 21 +++++++++++++--- packages/@lwc/features/src/index.ts | 1 + packages/@lwc/features/src/types.ts | 7 ++++++ .../@lwc/shared/src/__tests__/context.spec.ts | 6 +++++ .../@lwc/shared/src/__tests__/signals.spec.ts | 6 +++++ packages/@lwc/shared/src/context.ts | 17 +++++++++++++ packages/@lwc/shared/src/signals.ts | 18 +++++++++++++ packages/@lwc/ssr-runtime/src/context.ts | 6 ++++- 9 files changed, 100 insertions(+), 7 deletions(-) diff --git a/packages/@lwc/engine-core/src/framework/modules/context.ts b/packages/@lwc/engine-core/src/framework/modules/context.ts index 4e406f2125..f4dd5fdee3 100644 --- a/packages/@lwc/engine-core/src/framework/modules/context.ts +++ b/packages/@lwc/engine-core/src/framework/modules/context.ts @@ -12,6 +12,7 @@ import { ArrayFilter, ContextEventName, isTrustedContext, + legacyIsTrustedContext, type ContextProvidedCallback, type ContextBinding as IContextBinding, } from '@lwc/shared'; @@ -92,7 +93,17 @@ export function connectContext(vm: VM) { const enumerableKeys = keys(getPrototypeOf(component)); const contextfulKeys = ArrayFilter.call(enumerableKeys, (enumerableKey) => - isTrustedContext((component as any)[enumerableKey]) + /** + * The legacy validation behavior was that this check should only + * be performed for runtimes that have provided a trustedContext set. + * However, this resulted in a bug as all component properties were + * being considered context in environments where the trustedContext + * set had not been provided. The runtime flag has been added as a killswitch + * in case the fix needs to be reverted. + */ + lwcRuntimeFlags.ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION + ? legacyIsTrustedContext((component as any)[enumerableKey]) + : isTrustedContext((component as any)[enumerableKey]) ); if (contextfulKeys.length === 0) { @@ -128,7 +139,17 @@ export function disconnectContext(vm: VM) { const enumerableKeys = keys(getPrototypeOf(component)); const contextfulKeys = ArrayFilter.call(enumerableKeys, (enumerableKey) => - isTrustedContext((component as any)[enumerableKey]) + /** + * The legacy validation behavior was that this check should only + * be performed for runtimes that have provided a trustedContext set. + * However, this resulted in a bug as all component properties were + * being considered context in environments where the trustedContext + * set had not been provided. The runtime flag has been added as a killswitch + * in case the fix needs to be reverted. + */ + lwcRuntimeFlags.ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION + ? legacyIsTrustedContext((component as any)[enumerableKey]) + : isTrustedContext((component as any)[enumerableKey]) ); if (contextfulKeys.length === 0) { diff --git a/packages/@lwc/engine-core/src/framework/mutation-tracker.ts b/packages/@lwc/engine-core/src/framework/mutation-tracker.ts index 3ed830fc64..06b173fe20 100644 --- a/packages/@lwc/engine-core/src/framework/mutation-tracker.ts +++ b/packages/@lwc/engine-core/src/framework/mutation-tracker.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { isNull, isObject, isTrustedSignal } from '@lwc/shared'; +import { isNull, isObject, isTrustedSignal, legacyIsTrustedSignal } from '@lwc/shared'; import { ReactiveObserver, valueMutated, valueObserved } from '../libs/mutation-tracker'; import { subscribeToSignal } from '../libs/signal-tracker'; import type { Signal } from '@lwc/signals'; @@ -41,13 +41,26 @@ export function componentValueObserved(vm: VM, key: PropertyKey, target: any = { lwcRuntimeFlags.ENABLE_EXPERIMENTAL_SIGNALS && isObject(target) && !isNull(target) && - isTrustedSignal(target) && process.env.IS_BROWSER && // Only subscribe if a template is being rendered by the engine tro.isObserving() ) { - // Subscribe the template reactive observer's notify method, which will mark the vm as dirty and schedule hydration. - subscribeToSignal(component, target as Signal, tro.notify.bind(tro)); + /** + * The legacy validation behavior was that this check should only + * be performed for runtimes that have provided a trustedSignals set. + * However, this resulted in a bug as all component properties were + * being considered signals in environments where the trustedSignals + * set had not been defined. The runtime flag has been added as a killswitch + * in case the fix needs to be reverted. + */ + if ( + (lwcRuntimeFlags.ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION && + legacyIsTrustedSignal(target)) || + (!lwcRuntimeFlags.ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION && isTrustedSignal(target)) + ) { + // Subscribe the template reactive observer's notify method, which will mark the vm as dirty and schedule hydration. + subscribeToSignal(component, target as Signal, tro.notify.bind(tro)); + } } } diff --git a/packages/@lwc/features/src/index.ts b/packages/@lwc/features/src/index.ts index 2c4608d7b0..3b8c54543c 100644 --- a/packages/@lwc/features/src/index.ts +++ b/packages/@lwc/features/src/index.ts @@ -19,6 +19,7 @@ const features: FeatureFlagMap = { ENABLE_LEGACY_SCOPE_TOKENS: null, ENABLE_FORCE_SHADOW_MIGRATE_MODE: null, ENABLE_EXPERIMENTAL_SIGNALS: null, + ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION: null, DISABLE_SYNTHETIC_SHADOW: null, DISABLE_SCOPE_TOKEN_VALIDATION: null, LEGACY_LOCKER_ENABLED: null, diff --git a/packages/@lwc/features/src/types.ts b/packages/@lwc/features/src/types.ts index ab7984b7a8..f72698d373 100644 --- a/packages/@lwc/features/src/types.ts +++ b/packages/@lwc/features/src/types.ts @@ -70,6 +70,13 @@ export interface FeatureFlagMap { */ ENABLE_EXPERIMENTAL_SIGNALS: FeatureFlagValue; + /** + * If true, legacy signal validation is used, where all component properties are considered signals or context + * if a trustedSignalSet and trustedContextSet have not been provided via setTrustedSignalSet and setTrustedContextSet. + * This is a killswitch for a bug fix: #5492 + */ + ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION: FeatureFlagValue; + /** * If true, ignore `@lwc/synthetic-shadow` even if it's loaded on the page. Instead, run all components in * native shadow mode. diff --git a/packages/@lwc/shared/src/__tests__/context.spec.ts b/packages/@lwc/shared/src/__tests__/context.spec.ts index f6a08d8af6..74847d0741 100644 --- a/packages/@lwc/shared/src/__tests__/context.spec.ts +++ b/packages/@lwc/shared/src/__tests__/context.spec.ts @@ -12,6 +12,7 @@ describe('context', () => { let setTrustedContextSet: (signals: WeakSet) => void; let addTrustedContext: (signal: object) => void; let isTrustedContext: (target: object) => boolean; + let legacyIsTrustedContext: (target: object) => boolean; beforeEach(async () => { vi.resetModules(); @@ -21,6 +22,7 @@ describe('context', () => { setTrustedContextSet = contextModule.setTrustedContextSet; addTrustedContext = contextModule.addTrustedContext; isTrustedContext = contextModule.isTrustedContext; + legacyIsTrustedContext = contextModule.legacyIsTrustedContext; }); it('should set and get context keys', () => { @@ -99,5 +101,9 @@ describe('context', () => { it('should return false for all calls when trustedContexts is not set', () => { expect(isTrustedContext({})).toBe(false); }); + + it('legacyIsTrustedContext should return true when trustedContexts is not set', () => { + expect(legacyIsTrustedContext({})).toBe(true); + }); }); }); diff --git a/packages/@lwc/shared/src/__tests__/signals.spec.ts b/packages/@lwc/shared/src/__tests__/signals.spec.ts index 93a2f5f72e..81f5243d1f 100644 --- a/packages/@lwc/shared/src/__tests__/signals.spec.ts +++ b/packages/@lwc/shared/src/__tests__/signals.spec.ts @@ -10,6 +10,7 @@ describe('signals', () => { let setTrustedSignalSet: (signals: WeakSet) => void; let addTrustedSignal: (signal: object) => void; let isTrustedSignal: (target: object) => boolean; + let legacyIsTrustedSignal: (target: object) => boolean; beforeEach(async () => { vi.resetModules(); @@ -17,6 +18,7 @@ describe('signals', () => { setTrustedSignalSet = signalsModule.setTrustedSignalSet; addTrustedSignal = signalsModule.addTrustedSignal; isTrustedSignal = signalsModule.isTrustedSignal; + legacyIsTrustedSignal = signalsModule.legacyIsTrustedSignal; }); describe('setTrustedSignalSet', () => { @@ -56,5 +58,9 @@ describe('signals', () => { it('should return false for all calls when trustedSignals is not set', () => { expect(isTrustedSignal({})).toBe(false); }); + + it('legacyIsTrustedSignal should return true when trustedSignals is not set', () => { + expect(legacyIsTrustedSignal({})).toBe(true); + }); }); }); diff --git a/packages/@lwc/shared/src/context.ts b/packages/@lwc/shared/src/context.ts index 9ec70c716c..7356f21532 100644 --- a/packages/@lwc/shared/src/context.ts +++ b/packages/@lwc/shared/src/context.ts @@ -51,6 +51,23 @@ export function addTrustedContext(contextParticipant: object) { trustedContext?.add(contextParticipant); } +/** + * The legacy validation behavior was that this check should only + * be performed for runtimes that have provided a trustedContext set. + * However, this resulted in a bug as all component properties were + * being considered context in environments where the trustedContext + * set had not been provided. The runtime flag has been added as a killswitch + * in case the fix needs to be reverted. + */ +export function legacyIsTrustedContext(target: object): boolean { + if (!trustedContext) { + // The runtime didn't set a trustedContext set + // this check should only be performed for runtimes that care about filtering context participants to track + return true; + } + return trustedContext.has(target); +} + export function isTrustedContext(target: object): boolean { if (!trustedContext) { return false; diff --git a/packages/@lwc/shared/src/signals.ts b/packages/@lwc/shared/src/signals.ts index 49f78565ac..ed57102bb4 100644 --- a/packages/@lwc/shared/src/signals.ts +++ b/packages/@lwc/shared/src/signals.ts @@ -19,6 +19,24 @@ export function addTrustedSignal(signal: object) { trustedSignals?.add(signal); } +/** + * The legacy validation behavior was that this check should only + * be performed for runtimes that have provided a trustedSignals set. + * However, this resulted in a bug as all component properties were + * being considered signals in environments where the trustedSignals + * set had not been defined. The runtime flag has been added as a killswitch + * in case the fix needs to be reverted. + */ +export function legacyIsTrustedSignal(target: object): boolean { + if (!trustedSignals) { + // The runtime didn't set a trustedSignals set + // this check should only be performed for runtimes that care about filtering signals to track + // our default behavior should be to track all signals + return true; + } + return trustedSignals.has(target); +} + export function isTrustedSignal(target: object): boolean { if (!trustedSignals) { return false; diff --git a/packages/@lwc/ssr-runtime/src/context.ts b/packages/@lwc/ssr-runtime/src/context.ts index 280b2053ca..6bcf4961ee 100644 --- a/packages/@lwc/ssr-runtime/src/context.ts +++ b/packages/@lwc/ssr-runtime/src/context.ts @@ -8,6 +8,7 @@ import { type ContextProvidedCallback, type ContextBinding as IContextBinding, isTrustedContext, + legacyIsTrustedContext, getContextKeys, isUndefined, keys, @@ -66,8 +67,11 @@ export function connectContext(le: LightningElement) { const { connectContext } = contextKeys; const enumerableKeys = keys(le); + const contextfulKeys = ArrayFilter.call(enumerableKeys, (enumerableKey) => - isTrustedContext((le as any)[enumerableKey]) + lwcRuntimeFlags.ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION + ? legacyIsTrustedContext((le as any)[enumerableKey]) + : isTrustedContext((le as any)[enumerableKey]) ); if (contextfulKeys.length === 0) { From f862a2ba87259846035b7ffa411301c35217f8d8 Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Mon, 15 Sep 2025 14:31:18 -0600 Subject: [PATCH 3/9] fix: bundlesize --- scripts/bundlesize/bundlesize.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bundlesize/bundlesize.config.json b/scripts/bundlesize/bundlesize.config.json index 941a42f360..57c8efea07 100644 --- a/scripts/bundlesize/bundlesize.config.json +++ b/scripts/bundlesize/bundlesize.config.json @@ -2,7 +2,7 @@ "files": [ { "path": "packages/@lwc/engine-dom/dist/index.js", - "maxSize": "24.68KB" + "maxSize": "24.72KB" }, { "path": "packages/@lwc/synthetic-shadow/dist/index.js", From 0e8cdd4f5263acd1fd5db459caef4a83131d5656 Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Mon, 15 Sep 2025 14:42:37 -0600 Subject: [PATCH 4/9] fix: bundlesize --- scripts/bundlesize/bundlesize.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bundlesize/bundlesize.config.json b/scripts/bundlesize/bundlesize.config.json index 57c8efea07..624cd12230 100644 --- a/scripts/bundlesize/bundlesize.config.json +++ b/scripts/bundlesize/bundlesize.config.json @@ -2,7 +2,7 @@ "files": [ { "path": "packages/@lwc/engine-dom/dist/index.js", - "maxSize": "24.72KB" + "maxSize": "24.73KB" }, { "path": "packages/@lwc/synthetic-shadow/dist/index.js", From 0804c69848bacea563077170d6e3dc31f7e5125c Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Mon, 15 Sep 2025 18:23:10 -0600 Subject: [PATCH 5/9] fix: review comments --- .../@lwc/engine-core/src/framework/modules/context.ts | 4 ++-- .../@lwc/engine-core/src/framework/mutation-tracker.ts | 8 ++++---- packages/@lwc/shared/src/context.ts | 2 +- packages/@lwc/shared/src/signals.ts | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/@lwc/engine-core/src/framework/modules/context.ts b/packages/@lwc/engine-core/src/framework/modules/context.ts index f4dd5fdee3..8bbd0b6190 100644 --- a/packages/@lwc/engine-core/src/framework/modules/context.ts +++ b/packages/@lwc/engine-core/src/framework/modules/context.ts @@ -96,7 +96,7 @@ export function connectContext(vm: VM) { /** * The legacy validation behavior was that this check should only * be performed for runtimes that have provided a trustedContext set. - * However, this resulted in a bug as all component properties were + * However, this resulted in a bug as all object values were * being considered context in environments where the trustedContext * set had not been provided. The runtime flag has been added as a killswitch * in case the fix needs to be reverted. @@ -142,7 +142,7 @@ export function disconnectContext(vm: VM) { /** * The legacy validation behavior was that this check should only * be performed for runtimes that have provided a trustedContext set. - * However, this resulted in a bug as all component properties were + * However, this resulted in a bug as all object values were * being considered context in environments where the trustedContext * set had not been provided. The runtime flag has been added as a killswitch * in case the fix needs to be reverted. diff --git a/packages/@lwc/engine-core/src/framework/mutation-tracker.ts b/packages/@lwc/engine-core/src/framework/mutation-tracker.ts index 06b173fe20..d28bd0225e 100644 --- a/packages/@lwc/engine-core/src/framework/mutation-tracker.ts +++ b/packages/@lwc/engine-core/src/framework/mutation-tracker.ts @@ -48,15 +48,15 @@ export function componentValueObserved(vm: VM, key: PropertyKey, target: any = { /** * The legacy validation behavior was that this check should only * be performed for runtimes that have provided a trustedSignals set. - * However, this resulted in a bug as all component properties were + * However, this resulted in a bug as all object values were * being considered signals in environments where the trustedSignals * set had not been defined. The runtime flag has been added as a killswitch * in case the fix needs to be reverted. */ if ( - (lwcRuntimeFlags.ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION && - legacyIsTrustedSignal(target)) || - (!lwcRuntimeFlags.ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION && isTrustedSignal(target)) + lwcRuntimeFlags.ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION + ? legacyIsTrustedSignal(target) + : isTrustedSignal(target) ) { // Subscribe the template reactive observer's notify method, which will mark the vm as dirty and schedule hydration. subscribeToSignal(component, target as Signal, tro.notify.bind(tro)); diff --git a/packages/@lwc/shared/src/context.ts b/packages/@lwc/shared/src/context.ts index 7356f21532..d8f0adf168 100644 --- a/packages/@lwc/shared/src/context.ts +++ b/packages/@lwc/shared/src/context.ts @@ -54,7 +54,7 @@ export function addTrustedContext(contextParticipant: object) { /** * The legacy validation behavior was that this check should only * be performed for runtimes that have provided a trustedContext set. - * However, this resulted in a bug as all component properties were + * However, this resulted in a bug as all object values were * being considered context in environments where the trustedContext * set had not been provided. The runtime flag has been added as a killswitch * in case the fix needs to be reverted. diff --git a/packages/@lwc/shared/src/signals.ts b/packages/@lwc/shared/src/signals.ts index ed57102bb4..9a5cccd3e4 100644 --- a/packages/@lwc/shared/src/signals.ts +++ b/packages/@lwc/shared/src/signals.ts @@ -22,7 +22,7 @@ export function addTrustedSignal(signal: object) { /** * The legacy validation behavior was that this check should only * be performed for runtimes that have provided a trustedSignals set. - * However, this resulted in a bug as all component properties were + * However, this resulted in a bug as all object values were * being considered signals in environments where the trustedSignals * set had not been defined. The runtime flag has been added as a killswitch * in case the fix needs to be reverted. From ed9c66a8961a76ab914906163e679c4539b3ad1c Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Mon, 15 Sep 2025 18:23:41 -0600 Subject: [PATCH 6/9] fix: test coverage for signal/context validation behavior --- .../src/framework/__tests__/context.spec.ts | 93 +++++++++++++++++++ .../__tests__/mutation-tracker.spec.ts | 71 ++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 packages/@lwc/engine-core/src/framework/__tests__/context.spec.ts create mode 100644 packages/@lwc/engine-core/src/framework/__tests__/mutation-tracker.spec.ts diff --git a/packages/@lwc/engine-core/src/framework/__tests__/context.spec.ts b/packages/@lwc/engine-core/src/framework/__tests__/context.spec.ts new file mode 100644 index 0000000000..f26bd60297 --- /dev/null +++ b/packages/@lwc/engine-core/src/framework/__tests__/context.spec.ts @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2025, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ +import { describe, it, expect, vi, beforeAll, afterEach } from 'vitest'; +import { setFeatureFlagForTest } from '@lwc/features'; +import { setTrustedContextSet, setContextKeys } from '@lwc/shared'; +import { logWarnOnce } from '../../shared/logger'; +import { connectContext, disconnectContext } from '../modules/context'; + +// Mock the logger to avoid console output during tests +vi.mock('../../shared/logger', () => ({ + logWarnOnce: vi.fn(), +})); + +// Create mock component with a regular, non-contextful property +const mockComponent = {}; +Object.setPrototypeOf(mockComponent, { + regularProp: 'not contextful', +}); + +// Create mock renderer +const mockRenderer = { + registerContextProvider: vi.fn(), +}; + +// Create mock VM +const mockVM = { + component: mockComponent, + elm: null, + renderer: mockRenderer, +} as any; + +/** + * These tests test that properties are correctly validated within the connectContext and disconnectContext + * functions regardless of whether trusted context has been defined or not. + * Integration tests have been used for extensive coverage of the LWC context feature, but this particular + * scenario is best isolated and unit tested as it involves manipulation of the trusted context API. + */ +describe('context functions', () => { + beforeAll(() => { + const connectContext = Symbol('connectContext'); + const disconnectContext = Symbol('disconnectContext'); + setContextKeys({ connectContext, disconnectContext }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('without setting trusted context', () => { + it('should log a warning when trustedContext is not defined and connectContext is called with legacy signal context validation', () => { + setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', true); + connectContext(mockVM); + expect(logWarnOnce).toHaveBeenCalledWith( + 'Attempted to connect to trusted context but received the following error: component[contextfulKeys[i]][connectContext2] is not a function' + ); + }); + + it('should not log a warning when trustedContext is not defined and connectContext is called with non-legacy context validation', () => { + setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); + connectContext(mockVM); + expect(logWarnOnce).not.toHaveBeenCalled(); + }); + + it('should log a warning when trustedContext is not defined and disconnectContext is called with legacy signal context validation', () => { + setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', true); + disconnectContext(mockVM); + expect(logWarnOnce).toHaveBeenCalledWith( + 'Attempted to disconnect from trusted context but received the following error: component[contextfulKeys[i]][disconnectContext2] is not a function' + ); + }); + + it('should not log a warning when trustedContext is not defined and disconnectContext is called with non-legacy context validation', () => { + setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); + disconnectContext(mockVM); + expect(logWarnOnce).not.toHaveBeenCalled(); + }); + }); + + describe('with trusted context set', () => { + it('should not log warnings when trustedContext is defined', () => { + setTrustedContextSet(new WeakSet()); + setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', true); + connectContext(mockVM); + disconnectContext(mockVM); + setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); + expect(logWarnOnce).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/@lwc/engine-core/src/framework/__tests__/mutation-tracker.spec.ts b/packages/@lwc/engine-core/src/framework/__tests__/mutation-tracker.spec.ts new file mode 100644 index 0000000000..683f831f1d --- /dev/null +++ b/packages/@lwc/engine-core/src/framework/__tests__/mutation-tracker.spec.ts @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2019, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ +import { describe, it, expect, vi, afterEach, beforeEach, afterAll, beforeAll } from 'vitest'; +import { setTrustedSignalSet } from '@lwc/shared'; +import { setFeatureFlagForTest } from '@lwc/features'; +import { componentValueObserved } from '../../framework/mutation-tracker'; + +// Create a mock VM object with required properties +const mockVM = { + component: {}, + tro: { + isObserving: () => true, + }, +} as any; + +/** + * These tests check that properties are correctly validated within the mutation-tracker + * regardless of whether trusted context has been defined by a state manager or not. + * Integration tests have been used for extensive coverage of the LWC signals feature, but this particular + * scenario is best isolated and unit tested as it involves manipulation of the trusted context API. + */ +describe('mutation-tracker', () => { + it('should not throw when componentValueObserved is called using the new signals validation and no signal set is defined', () => { + setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); + expect(() => { + componentValueObserved(mockVM, 'testKey', {}); + }).not.toThrow(); + }); + + it('should throw when componentValueObserved is called using legacy signals validation and no signal set has been defined', () => { + setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', true); + expect(() => { + componentValueObserved(mockVM, 'testKey', {}); + }).toThrow(); + }); + + it('should not throw when a trusted signal set is defined abd componentValueObserved is called', () => { + setTrustedSignalSet(new WeakSet()); + + setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); + expect(() => { + componentValueObserved(mockVM, 'testKey', {}); + }).not.toThrow(); + + setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', true); + expect(() => { + componentValueObserved(mockVM, 'testKey', {}); + }).not.toThrow(); + }); + + beforeAll(() => { + setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', true); + }); + + afterAll(() => { + setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', false); + setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); + }); + + beforeEach(() => { + vi.stubEnv('IS_BROWSER', 'true'); // Signals is a browser-only feature + }); + + afterEach(() => { + vi.unstubAllEnvs(); // Reset environment variables after each test + }); +}); From 8be0a75baa2c3ea4fc47a45e6176464155b37b27 Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Mon, 15 Sep 2025 21:24:21 -0600 Subject: [PATCH 7/9] fix: vitest prod mode compat --- package.json | 1 + .../src/framework/__tests__/context.spec.ts | 32 +++++++++++++------ .../__tests__/mutation-tracker.spec.ts | 28 ++++++++++------ 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 11081c1d41..71f65b24a1 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "dev": "nx run-many --target=dev --all --parallel=999 --exclude=@lwc/perf-benchmarks,@lwc/perf-benchmarks-components,@lwc/integration-tests", "test": "vitest --workspace vitest.workspace.mjs", "test:production": "VITE_NODE_ENV=production vitest --workspace vitest.workspace.mjs", + "test:production:debug": "VITE_NODE_ENV=production vitest --workspace vitest.workspace.mjs --no-file-parallelism --inspect-brk", "test:bespoke": "nx run-many --target=test", "test:debug": "vitest --workspace vitest.workspace.mjs --inspect-brk --no-file-parallelism", "test:ci": "vitest run --workspace vitest.workspace.mjs --coverage", diff --git a/packages/@lwc/engine-core/src/framework/__tests__/context.spec.ts b/packages/@lwc/engine-core/src/framework/__tests__/context.spec.ts index f26bd60297..c1e79560a3 100644 --- a/packages/@lwc/engine-core/src/framework/__tests__/context.spec.ts +++ b/packages/@lwc/engine-core/src/framework/__tests__/context.spec.ts @@ -4,13 +4,12 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { describe, it, expect, vi, beforeAll, afterEach } from 'vitest'; -import { setFeatureFlagForTest } from '@lwc/features'; +import { describe, it, expect, vi, beforeAll, afterEach, afterAll } from 'vitest'; import { setTrustedContextSet, setContextKeys } from '@lwc/shared'; import { logWarnOnce } from '../../shared/logger'; import { connectContext, disconnectContext } from '../modules/context'; -// Mock the logger to avoid console output during tests +// Mock the logger to inspect console output during tests vi.mock('../../shared/logger', () => ({ logWarnOnce: vi.fn(), })); @@ -33,6 +32,17 @@ const mockVM = { renderer: mockRenderer, } as any; +if (!(globalThis as any).lwcRuntimeFlags) { + Object.defineProperty(globalThis, 'lwcRuntimeFlags', { value: {} }); +} + +/** + * Need to be able to set and reset the flags at will (lwc/features doesn't provide this) + */ +const setFeatureFlag = (name: string, value: boolean) => { + (globalThis as any).lwcRuntimeFlags[name] = value; +}; + /** * These tests test that properties are correctly validated within the connectContext and disconnectContext * functions regardless of whether trusted context has been defined or not. @@ -50,9 +60,13 @@ describe('context functions', () => { vi.clearAllMocks(); }); + afterAll(() => { + setFeatureFlag('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); + }); + describe('without setting trusted context', () => { it('should log a warning when trustedContext is not defined and connectContext is called with legacy signal context validation', () => { - setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', true); + setFeatureFlag('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', true); connectContext(mockVM); expect(logWarnOnce).toHaveBeenCalledWith( 'Attempted to connect to trusted context but received the following error: component[contextfulKeys[i]][connectContext2] is not a function' @@ -60,13 +74,13 @@ describe('context functions', () => { }); it('should not log a warning when trustedContext is not defined and connectContext is called with non-legacy context validation', () => { - setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); + setFeatureFlag('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); connectContext(mockVM); expect(logWarnOnce).not.toHaveBeenCalled(); }); it('should log a warning when trustedContext is not defined and disconnectContext is called with legacy signal context validation', () => { - setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', true); + setFeatureFlag('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', true); disconnectContext(mockVM); expect(logWarnOnce).toHaveBeenCalledWith( 'Attempted to disconnect from trusted context but received the following error: component[contextfulKeys[i]][disconnectContext2] is not a function' @@ -74,7 +88,7 @@ describe('context functions', () => { }); it('should not log a warning when trustedContext is not defined and disconnectContext is called with non-legacy context validation', () => { - setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); + setFeatureFlag('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); disconnectContext(mockVM); expect(logWarnOnce).not.toHaveBeenCalled(); }); @@ -83,10 +97,10 @@ describe('context functions', () => { describe('with trusted context set', () => { it('should not log warnings when trustedContext is defined', () => { setTrustedContextSet(new WeakSet()); - setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', true); + setFeatureFlag('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', true); connectContext(mockVM); disconnectContext(mockVM); - setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); + setFeatureFlag('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); expect(logWarnOnce).not.toHaveBeenCalled(); }); }); diff --git a/packages/@lwc/engine-core/src/framework/__tests__/mutation-tracker.spec.ts b/packages/@lwc/engine-core/src/framework/__tests__/mutation-tracker.spec.ts index 683f831f1d..889d0de447 100644 --- a/packages/@lwc/engine-core/src/framework/__tests__/mutation-tracker.spec.ts +++ b/packages/@lwc/engine-core/src/framework/__tests__/mutation-tracker.spec.ts @@ -6,7 +6,6 @@ */ import { describe, it, expect, vi, afterEach, beforeEach, afterAll, beforeAll } from 'vitest'; import { setTrustedSignalSet } from '@lwc/shared'; -import { setFeatureFlagForTest } from '@lwc/features'; import { componentValueObserved } from '../../framework/mutation-tracker'; // Create a mock VM object with required properties @@ -17,6 +16,17 @@ const mockVM = { }, } as any; +if (!(globalThis as any).lwcRuntimeFlags) { + Object.defineProperty(globalThis, 'lwcRuntimeFlags', { value: {} }); +} + +/** + * Need to be able to set and reset the flags at will (lwc/features doesn't provide this) + */ +const setFeatureFlag = (name: string, value: boolean) => { + (globalThis as any).lwcRuntimeFlags[name] = value; +}; + /** * These tests check that properties are correctly validated within the mutation-tracker * regardless of whether trusted context has been defined by a state manager or not. @@ -25,14 +35,14 @@ const mockVM = { */ describe('mutation-tracker', () => { it('should not throw when componentValueObserved is called using the new signals validation and no signal set is defined', () => { - setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); + setFeatureFlag('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); expect(() => { componentValueObserved(mockVM, 'testKey', {}); }).not.toThrow(); }); it('should throw when componentValueObserved is called using legacy signals validation and no signal set has been defined', () => { - setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', true); + setFeatureFlag('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', true); expect(() => { componentValueObserved(mockVM, 'testKey', {}); }).toThrow(); @@ -40,25 +50,25 @@ describe('mutation-tracker', () => { it('should not throw when a trusted signal set is defined abd componentValueObserved is called', () => { setTrustedSignalSet(new WeakSet()); - - setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); + setFeatureFlag('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); expect(() => { componentValueObserved(mockVM, 'testKey', {}); }).not.toThrow(); - setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', true); + setFeatureFlag('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', true); expect(() => { componentValueObserved(mockVM, 'testKey', {}); }).not.toThrow(); }); beforeAll(() => { - setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', true); + setFeatureFlag('ENABLE_EXPERIMENTAL_SIGNALS', true); + setFeatureFlag('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', true); }); afterAll(() => { - setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', false); - setFeatureFlagForTest('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); + setFeatureFlag('ENABLE_EXPERIMENTAL_SIGNALS', false); + setFeatureFlag('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); }); beforeEach(() => { From 550da055333357f3065646f3b3c4172e214f6a1a Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Mon, 15 Sep 2025 21:33:43 -0600 Subject: [PATCH 8/9] fix: test typos --- .../src/framework/__tests__/context.spec.ts | 1 + .../__tests__/mutation-tracker.spec.ts | 47 ++++++++++--------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/packages/@lwc/engine-core/src/framework/__tests__/context.spec.ts b/packages/@lwc/engine-core/src/framework/__tests__/context.spec.ts index c1e79560a3..618c835d94 100644 --- a/packages/@lwc/engine-core/src/framework/__tests__/context.spec.ts +++ b/packages/@lwc/engine-core/src/framework/__tests__/context.spec.ts @@ -48,6 +48,7 @@ const setFeatureFlag = (name: string, value: boolean) => { * functions regardless of whether trusted context has been defined or not. * Integration tests have been used for extensive coverage of the LWC context feature, but this particular * scenario is best isolated and unit tested as it involves manipulation of the trusted context API. + * See bug fix: #5492 */ describe('context functions', () => { beforeAll(() => { diff --git a/packages/@lwc/engine-core/src/framework/__tests__/mutation-tracker.spec.ts b/packages/@lwc/engine-core/src/framework/__tests__/mutation-tracker.spec.ts index 889d0de447..9fe774772c 100644 --- a/packages/@lwc/engine-core/src/framework/__tests__/mutation-tracker.spec.ts +++ b/packages/@lwc/engine-core/src/framework/__tests__/mutation-tracker.spec.ts @@ -32,33 +32,38 @@ const setFeatureFlag = (name: string, value: boolean) => { * regardless of whether trusted context has been defined by a state manager or not. * Integration tests have been used for extensive coverage of the LWC signals feature, but this particular * scenario is best isolated and unit tested as it involves manipulation of the trusted context API. + * See bug fix: #5492 */ describe('mutation-tracker', () => { - it('should not throw when componentValueObserved is called using the new signals validation and no signal set is defined', () => { - setFeatureFlag('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); - expect(() => { - componentValueObserved(mockVM, 'testKey', {}); - }).not.toThrow(); - }); + describe('trustedSignal set not defined', () => { + it('should not throw when componentValueObserved is called using the new signals validation', () => { + setFeatureFlag('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); + expect(() => { + componentValueObserved(mockVM, 'testKey', {}); + }).not.toThrow(); + }); - it('should throw when componentValueObserved is called using legacy signals validation and no signal set has been defined', () => { - setFeatureFlag('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', true); - expect(() => { - componentValueObserved(mockVM, 'testKey', {}); - }).toThrow(); + it('should throw when componentValueObserved is called using legacy signals validation', () => { + setFeatureFlag('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', true); + expect(() => { + componentValueObserved(mockVM, 'testKey', {}); + }).toThrow(); + }); }); - it('should not throw when a trusted signal set is defined abd componentValueObserved is called', () => { - setTrustedSignalSet(new WeakSet()); - setFeatureFlag('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); - expect(() => { - componentValueObserved(mockVM, 'testKey', {}); - }).not.toThrow(); + describe('trustedSignal set defined', () => { + it('should not throw when componentValueObserved is called, regardless of validation type', () => { + setTrustedSignalSet(new WeakSet()); + setFeatureFlag('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); + expect(() => { + componentValueObserved(mockVM, 'testKey', {}); + }).not.toThrow(); - setFeatureFlag('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', true); - expect(() => { - componentValueObserved(mockVM, 'testKey', {}); - }).not.toThrow(); + setFeatureFlag('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', true); + expect(() => { + componentValueObserved(mockVM, 'testKey', {}); + }).not.toThrow(); + }); }); beforeAll(() => { From 273426829d492409d160f790abc9aeec579c80d3 Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Mon, 15 Sep 2025 21:52:23 -0600 Subject: [PATCH 9/9] fix: missing test --- package.json | 1 - .../@lwc/engine-core/src/framework/__tests__/context.spec.ts | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 71f65b24a1..11081c1d41 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "dev": "nx run-many --target=dev --all --parallel=999 --exclude=@lwc/perf-benchmarks,@lwc/perf-benchmarks-components,@lwc/integration-tests", "test": "vitest --workspace vitest.workspace.mjs", "test:production": "VITE_NODE_ENV=production vitest --workspace vitest.workspace.mjs", - "test:production:debug": "VITE_NODE_ENV=production vitest --workspace vitest.workspace.mjs --no-file-parallelism --inspect-brk", "test:bespoke": "nx run-many --target=test", "test:debug": "vitest --workspace vitest.workspace.mjs --inspect-brk --no-file-parallelism", "test:ci": "vitest run --workspace vitest.workspace.mjs --coverage", diff --git a/packages/@lwc/engine-core/src/framework/__tests__/context.spec.ts b/packages/@lwc/engine-core/src/framework/__tests__/context.spec.ts index 618c835d94..a8b098e2d9 100644 --- a/packages/@lwc/engine-core/src/framework/__tests__/context.spec.ts +++ b/packages/@lwc/engine-core/src/framework/__tests__/context.spec.ts @@ -102,6 +102,8 @@ describe('context functions', () => { connectContext(mockVM); disconnectContext(mockVM); setFeatureFlag('ENABLE_LEGACY_SIGNAL_CONTEXT_VALIDATION', false); + connectContext(mockVM); + disconnectContext(mockVM); expect(logWarnOnce).not.toHaveBeenCalled(); }); });