diff --git a/src/runtime/proxy-component.ts b/src/runtime/proxy-component.ts
index e9a916b6d9e..d3e276d7dd7 100644
--- a/src/runtime/proxy-component.ts
+++ b/src/runtime/proxy-component.ts
@@ -2,7 +2,7 @@ import type * as d from '../declarations';
import { BUILD } from '@app-data';
import { consoleDevWarn, getHostRef, plt } from '@platform';
import { getValue, setValue } from './set-value';
-import { MEMBER_FLAGS } from '../utils/constants';
+import { HOST_FLAGS, MEMBER_FLAGS } from '../utils/constants';
import { PROXY_FLAGS } from './runtime-constants';
export const proxyComponent = (Cstr: d.ComponentConstructor, cmpMeta: d.ComponentRuntimeMeta, flags: number) => {
@@ -23,15 +23,21 @@ export const proxyComponent = (Cstr: d.ComponentConstructor, cmpMeta: d.Componen
return getValue(this, memberName);
},
set(this: d.RuntimeRef, newValue) {
- if (
- // only during dev time
- BUILD.isDev &&
- // we are proxing the instance (not element)
- (flags & PROXY_FLAGS.isElementConstructor) === 0 &&
- // the member is a non-mutable prop
- (memberFlags & (MEMBER_FLAGS.Prop | MEMBER_FLAGS.Mutable)) === MEMBER_FLAGS.Prop
- ) {
- consoleDevWarn(`@Prop() "${memberName}" on "${cmpMeta.$tagName$}" cannot be modified.\nFurther information: https://stenciljs.com/docs/properties#prop-mutability`);
+ // only during dev time
+ if (BUILD.isDev) {
+ const ref = getHostRef(this);
+ if (
+ // we are proxying the instance (not element)
+ (flags & PROXY_FLAGS.isElementConstructor) === 0 &&
+ // the element is not constructing
+ (ref.$flags$ & HOST_FLAGS.isConstructingInstance) === 0 &&
+ // the member is a prop
+ (memberFlags & MEMBER_FLAGS.Prop) !== 0 &&
+ // the member is not mutable
+ (memberFlags & MEMBER_FLAGS.Mutable) === 0
+ ) {
+ consoleDevWarn(`@Prop() "${memberName}" on <${cmpMeta.$tagName$}> is immutable but was modified from within the component.\nMore information: https://stenciljs.com/docs/properties#prop-mutability`);
+ }
}
// proxyComponent, set value
setValue(this, memberName, newValue, cmpMeta);
diff --git a/src/runtime/test/prop-warnings.spec.tsx b/src/runtime/test/prop-warnings.spec.tsx
new file mode 100644
index 00000000000..18ee62f6957
--- /dev/null
+++ b/src/runtime/test/prop-warnings.spec.tsx
@@ -0,0 +1,92 @@
+import { Component, Prop, Method, h } from '@stencil/core';
+import { newSpecPage } from '@stencil/core/testing';
+
+describe('prop', () => {
+ const spy = jest.spyOn(console, 'warn').mockImplementation();
+
+ afterEach(() => spy.mockReset());
+ afterAll(() => spy.mockRestore());
+
+ it('should show warning when immutable prop is mutated', async () => {
+ @Component({ tag: 'cmp-a' })
+ class CmpA {
+ @Prop() a = 1;
+
+ @Method()
+ async update() {
+ this.a = 2;
+ }
+
+ render() {
+ return `${this.a}`;
+ }
+ }
+
+ const { root, waitForChanges } = await newSpecPage({
+ components: [CmpA],
+ html: ``,
+ });
+
+ expect(root).toEqualHtml('1');
+
+ await root.update();
+ await waitForChanges();
+
+ expect(root).toEqualHtml('2');
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(spy.mock.calls[0][0]).toMatch(/@Prop\(\) "[A-Za-z-]+" on <[A-Za-z-]+> is immutable/);
+ });
+
+ it('should not show warning when mutable prop is mutated', async () => {
+ @Component({ tag: 'cmp-a' })
+ class CmpA {
+ @Prop({ mutable: true }) a = 1;
+
+ @Method()
+ async update() {
+ this.a = 2;
+ }
+
+ render() {
+ return `${this.a}`;
+ }
+ }
+
+ const { root, waitForChanges } = await newSpecPage({
+ components: [CmpA],
+ html: ``,
+ });
+
+ expect(root).toEqualHtml('1');
+
+ await root.update();
+ await waitForChanges();
+
+ expect(root).toEqualHtml('2');
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('should not show warning when immutable prop is mutated from parent', async () => {
+ @Component({ tag: 'cmp-a' })
+ class CmpA {
+ @Prop() a = 1;
+
+ render() {
+ return `${this.a}`;
+ }
+ }
+
+ const { root, waitForChanges } = await newSpecPage({
+ components: [CmpA],
+ html: ``,
+ });
+
+ expect(root).toEqualHtml('1');
+
+ root.a = 2;
+ await waitForChanges();
+
+ expect(root).toEqualHtml('2');
+ expect(spy).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/testing/platform/testing-log.ts b/src/testing/platform/testing-log.ts
index cbe5fa5a35a..6e3d712c963 100644
--- a/src/testing/platform/testing-log.ts
+++ b/src/testing/platform/testing-log.ts
@@ -14,12 +14,13 @@ export const consoleDevError = (...e: any[]) => {
caughtErrors.push(new Error(e.join(', ')));
};
-export const consoleDevWarn = (..._: any[]) => {
- /* noop for testing */
+export const consoleDevWarn = (args: any) => {
+ // log warnings so we can spy on them when testing
+ console.warn(args);
};
export const consoleDevInfo = (..._: any[]) => {
/* noop for testing */
};
-export const setErrorHandler = (handler: d.ErrorHandler) => customError = handler;
+export const setErrorHandler = (handler: d.ErrorHandler) => (customError = handler);