Repository: vuejs/vue-next (ts)

Issue:
select option value attribute is not set if value is equal to text
### Version
3.2.22

### Reproduction link
[sfc.vuejs.org/](https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHRlbXBsYXRlPlxuICA8c2VsZWN0PlxuICAgXHQ8b3B0aW9uIHZhbHVlPVwiZm9vMVwiPmZvbzE8L29wdGlvbj5cbiAgIFx0PG9wdGlvbiB2YWx1ZT1cImZvbzJcIj5mb288L29wdGlvbj5cbiAgPC9zZWxlY3Q+XG48L3RlbXBsYXRlPiIsImltcG9ydC1tYXAuanNvbiI6IntcbiAgXCJpbXBvcnRzXCI6IHtcbiAgICBcInZ1ZVwiOiBcImh0dHBzOi8vc2ZjLnZ1ZWpzLm9yZy92dWUucnVudGltZS5lc20tYnJvd3Nlci5qc1wiXG4gIH1cbn0ifQ==)







### Steps to reproduce
Inspect the select options and see that the option with `value === textContent` has no value set. 

### What is expected?
Regardless of the textContent I expected an option to correctly have the value attribute and not silently remove it.

### What is actually happening?
The value attribute is removed.

---
I have verified that this is not happening in vue2. [Reproduction link](https://codesandbox.io/s/vue?file=/src/App.vue)

If I'm missing some part of the [option spec](https://www.w3.org/TR/2011/WD-html5-author-20110809/the-option-element.html) please point me to it. But as far as I am seeing it it is perfectly valid to have an option with value = textContent and I our case needed for e2e testing frameworks like Selenium to select a predefined value.

<!-- generated by vue-issues. DO NOT REMOVE -->

Pull request description:
Ensure value for OPTION elements is properly set in edge case (fix #4956)
This PR covers an edge case where the OPTION element has a `textContent` matching its assigned `value` attribute.

Previously, Vue would fail to properly assign the value in this case (and consequently, the `value` attribute was missing), because for OPTION elements, `el.value` falls back to `el.textContent` when no distinct value has been set (yet)

close #4956

Patch:
diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts
index 29367007c..0fa1c4bc2 100644
--- a/packages/reactivity/src/baseHandlers.ts
+++ b/packages/reactivity/src/baseHandlers.ts
@@ -7,7 +7,8 @@ import {
   readonlyMap,
   reactiveMap,
   shallowReactiveMap,
-  shallowReadonlyMap
+  shallowReadonlyMap,
+  isReadonly
 } from './reactive'
 import { TrackOpTypes, TriggerOpTypes } from './operations'
 import {
@@ -146,7 +147,7 @@ function createSetter(shallow = false) {
     receiver: object
   ): boolean {
     let oldValue = (target as any)[key]
-    if (!shallow) {
+    if (!shallow && !isReadonly(value)) {
       value = toRaw(value)
       oldValue = toRaw(oldValue)
       if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts
index 231e15e60..351fc0c26 100644
--- a/packages/reactivity/src/effect.ts
+++ b/packages/reactivity/src/effect.ts
@@ -23,7 +23,7 @@ let effectTrackDepth = 0
 export let trackOpBit = 1
 
 /**
- * The bitwise track markers support at most 30 levels op recursion.
+ * The bitwise track markers support at most 30 levels of recursion.
  * This value is chosen to enable modern JS engines to use a SMI on all platforms.
  * When recursion depth is greater, fall back to using a full cleanup.
  */
diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts
index fb43668e4..93eb4fa89 100644
--- a/packages/reactivity/src/ref.ts
+++ b/packages/reactivity/src/ref.ts
@@ -190,9 +190,7 @@ export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
 }
 
 export type ToRefs<T = any> = {
-  // #2687: somehow using ToRef<T[K]> here turns the resulting type into
-  // a union of multiple Ref<*> types instead of a single Ref<* | *> type.
-  [K in keyof T]: T[K] extends Ref ? T[K] : Ref<UnwrapRef<T[K]>>
+  [K in keyof T]: ToRef<T[K]>
 }
 export function toRefs<T extends object>(object: T): ToRefs<T> {
   if (__DEV__ && !isProxy(object)) {
diff --git a/packages/runtime-core/src/apiSetupHelpers.ts b/packages/runtime-core/src/apiSetupHelpers.ts
index fc1866af9..02ca5d53e 100644
--- a/packages/runtime-core/src/apiSetupHelpers.ts
+++ b/packages/runtime-core/src/apiSetupHelpers.ts
@@ -131,6 +131,7 @@ type InferDefaults<T> = {
 }
 
 type InferDefault<P, T> = T extends
+  | null
   | number
   | string
   | boolean
diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts
index 924f40a73..c99949222 100644
--- a/packages/runtime-core/src/componentProps.ts
+++ b/packages/runtime-core/src/componentProps.ts
@@ -39,6 +39,7 @@ import { createPropsDefaultThis } from './compat/props'
 import { isCompatEnabled, softAssertCompatEnabled } from './compat/compatConfig'
 import { DeprecationTypes } from './compat/compatConfig'
 import { shouldSkipAttr } from './compat/attrsFallthrough'
+import { IfAny } from './helpers/typeUtils'
 
 export type ComponentPropsOptions<P = Data> =
   | ComponentObjectPropsOptions<P>
@@ -115,7 +116,7 @@ type InferPropType<T> = [T] extends [null]
     : InferPropType<U>
   : [T] extends [Prop<infer V, infer D>]
   ? unknown extends V
-    ? D
+    ? IfAny<V, V, D>
     : V
   : T
 
diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts
index 30ad4400d..223a7c602 100644
--- a/packages/runtime-core/src/components/KeepAlive.ts
+++ b/packages/runtime-core/src/components/KeepAlive.ts
@@ -381,7 +381,7 @@ function registerKeepAliveHook(
         }
         current = current.parent
       }
-      hook()
+      return hook()
     })
   injectHook(type, wrappedHook, target)
   // In addition to registering it on the target instance, we walk up the parent
diff --git a/packages/runtime-core/src/helpers/typeUtils.ts b/packages/runtime-core/src/helpers/typeUtils.ts
index 204543e6d..8caba54c6 100644
--- a/packages/runtime-core/src/helpers/typeUtils.ts
+++ b/packages/runtime-core/src/helpers/typeUtils.ts
@@ -6,3 +6,5 @@ export type UnionToIntersection<U> = (
 
 // make keys required but keep undefined values
 export type LooseRequired<T> = { [P in string & keyof T]: T[P] }
+
+export type IfAny<T, Y, N> = 0 extends (1 & T) ? Y : N
diff --git a/packages/runtime-dom/src/modules/props.ts b/packages/runtime-dom/src/modules/props.ts
index d70692396..63513f347 100644
--- a/packages/runtime-dom/src/modules/props.ts
+++ b/packages/runtime-dom/src/modules/props.ts
@@ -31,7 +31,13 @@ export function patchDOMProp(
     // non-string values will be stringified.
     el._value = value
     const newValue = value == null ? '' : value
-    if (el.value !== newValue) {
+    if (
+      el.value !== newValue ||
+      // #4956: always set for OPTION elements because its value falls back to
+      // textContent if no value attribute is present. And setting .value for
+      // OPTION has no side effect
+      el.tagName === 'OPTION'
+    ) {
       el.value = newValue
     }
     if (value == null) {


Test patch:
diff --git a/packages/compiler-sfc/__tests__/compileScript.spec.ts b/packages/compiler-sfc/__tests__/compileScript.spec.ts
index 694619174..85b9f9a41 100644
--- a/packages/compiler-sfc/__tests__/compileScript.spec.ts
+++ b/packages/compiler-sfc/__tests__/compileScript.spec.ts
@@ -292,7 +292,7 @@ defineExpose({ foo: 123 })
       let foo = $ref(1)
       </script>
       `,
-        { refSugar: true }
+        { refTransform: true }
       )
       assertCode(content)
       expect(content).toMatch(`import { ref } from 'vue'`)
@@ -1090,7 +1090,7 @@ const emit = defineEmits(['a', 'b'])
   describe('async/await detection', () => {
     function assertAwaitDetection(code: string, shouldAsync = true) {
       const { content } = compile(`<script setup>${code}</script>`, {
-        refSugar: true
+        refTransform: true
       })
       if (shouldAsync) {
         expect(content).toMatch(`let __temp, __restore`)
diff --git a/packages/compiler-sfc/__tests__/compileScriptRefTransform.spec.ts b/packages/compiler-sfc/__tests__/compileScriptRefTransform.spec.ts
index 34dc9efea..d2f525890 100644
--- a/packages/compiler-sfc/__tests__/compileScriptRefTransform.spec.ts
+++ b/packages/compiler-sfc/__tests__/compileScriptRefTransform.spec.ts
@@ -5,7 +5,7 @@ import { compileSFCScript as compile, assertCode } from './utils'
 // transform can be found in <root>/packages/ref-transform/__tests__
 describe('sfc ref transform', () => {
   function compileWithRefTransform(src: string) {
-    return compile(src, { refSugar: true })
+    return compile(src, { refTransform: true })
   }
 
   test('$ unwrapping', () => {
@@ -156,7 +156,7 @@ describe('sfc ref transform', () => {
           bar
         })
       </script>`,
-          { refSugar: true }
+          { refTransform: true }
         )
       ).toThrow(`cannot reference locally declared variables`)
 
@@ -168,7 +168,7 @@ describe('sfc ref transform', () => {
           bar
         })
       </script>`,
-          { refSugar: true }
+          { refTransform: true }
         )
       ).toThrow(`cannot reference locally declared variables`)
     })
diff --git a/packages/reactivity/__tests__/readonly.spec.ts b/packages/reactivity/__tests__/readonly.spec.ts
index 63426bb5e..185bb3107 100644
--- a/packages/reactivity/__tests__/readonly.spec.ts
+++ b/packages/reactivity/__tests__/readonly.spec.ts
@@ -465,4 +465,13 @@ describe('reactivity/readonly', () => {
       'Set operation on key "randomProperty" failed: target is readonly.'
     ).toHaveBeenWarned()
   })
+
+  // #4986
+  test('setting a readonly object as a property of a reactive object should retain readonly proxy', () => {
+    const r = readonly({})
+    const rr = reactive({}) as any
+    rr.foo = r
+    expect(rr.foo).toBe(r)
+    expect(isReadonly(rr.foo)).toBe(true)
+  })
 })
diff --git a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts
index 4dce5d144..b60931e1f 100644
--- a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts
+++ b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts
@@ -16,7 +16,9 @@ import {
   cloneVNode,
   provide,
   defineAsyncComponent,
-  Component
+  Component,
+  createApp,
+  onActivated
 } from '@vue/runtime-test'
 import { KeepAliveProps } from '../../src/components/KeepAlive'
 
@@ -874,4 +876,31 @@ describe('KeepAlive', () => {
     await nextTick()
     expect(serializeInner(root)).toBe('<p>1</p>')
   })
+
+  // #4976
+  test('handle error in async onActivated', async () => {
+    const err = new Error('foo')
+    const handler = jest.fn()
+
+    const app = createApp({
+      setup() {
+        return () => h(KeepAlive, null, () => h(Child))
+      }
+    })
+
+    const Child = {
+      setup() {
+        onActivated(async () => {
+          throw err
+        })
+      },
+      render() {}
+    }
+
+    app.config.errorHandler = handler
+    app.mount(nodeOps.createElement('div'))
+
+    await nextTick()
+    expect(handler).toHaveBeenCalledWith(err, {}, 'activated hook')
+  })
 })
diff --git a/packages/runtime-dom/__tests__/patchProps.spec.ts b/packages/runtime-dom/__tests__/patchProps.spec.ts
index e1ee7928c..e2590e2e0 100644
--- a/packages/runtime-dom/__tests__/patchProps.spec.ts
+++ b/packages/runtime-dom/__tests__/patchProps.spec.ts
@@ -29,9 +29,12 @@ describe('runtime-dom: props patching', () => {
   // so we need to add tests for other elements
   test('value for non-text input', () => {
     const el = document.createElement('option')
+    el.textContent = 'foo' // #4956
     patchProp(el, 'value', null, 'foo')
+    expect(el.getAttribute('value')).toBe('foo')
     expect(el.value).toBe('foo')
     patchProp(el, 'value', null, null)
+    el.textContent = ''
     expect(el.value).toBe('')
     // #3475
     expect(el.getAttribute('value')).toBe(null)
diff --git a/test-dts/component.test-d.ts b/test-dts/component.test-d.ts
index 93c3ea410..d8d3934b9 100644
--- a/test-dts/component.test-d.ts
+++ b/test-dts/component.test-d.ts
@@ -10,7 +10,8 @@ import {
   ShallowUnwrapRef,
   FunctionalComponent,
   ComponentPublicInstance,
-  toRefs
+  toRefs,
+  IsAny
 } from './index'
 
 declare function extractComponentOptions<Props, RawBindings>(
@@ -62,6 +63,7 @@ describe('object props', () => {
     ffff: Ref<(a: number, b: string) => { a: boolean }>
     validated: Ref<string | undefined>
     object: Ref<object | undefined>
+    zzz: any
   }
 
   describe('defineComponent', () => {
@@ -130,7 +132,8 @@ describe('object props', () => {
           // validator requires explicit annotation
           validator: (val: unknown) => val !== ''
         },
-        object: Object as PropType<object>
+        object: Object as PropType<object>,
+        zzz: Object as PropType<any>
       },
       setup(props) {
         const refs = toRefs(props)
@@ -152,6 +155,7 @@ describe('object props', () => {
         expectType<ExpectedRefs['ffff']>(refs.ffff)
         expectType<ExpectedRefs['validated']>(refs.validated)
         expectType<ExpectedRefs['object']>(refs.object)
+        expectType<IsAny<typeof props.zzz>>(true)
 
         return {
           setupA: 1,
diff --git a/test-dts/index.d.ts b/test-dts/index.d.ts
index 3d8d288fa..59eadcb92 100644
--- a/test-dts/index.d.ts
+++ b/test-dts/index.d.ts
@@ -14,3 +14,5 @@ export type IsUnion<T, U extends T = T> = (T extends any
   : never) extends false
   ? false
   : true
+
+export type IsAny<T> = 0 extends (1 & T) ? true : false
diff --git a/test-dts/ref.test-d.ts b/test-dts/ref.test-d.ts
index d4462db9e..5b5274c98 100644
--- a/test-dts/ref.test-d.ts
+++ b/test-dts/ref.test-d.ts
@@ -10,8 +10,7 @@ import {
   toRef,
   toRefs,
   ToRefs,
-  shallowReactive,
-  watch
+  shallowReactive
 } from './index'
 
 function plainType(arg: number | Ref<number>) {
@@ -185,28 +184,45 @@ const p2 = proxyRefs(r2)
 expectType<number>(p2.a)
 expectType<Ref<string>>(p2.obj.k)
 
-// toRef
-const obj = {
-  a: 1,
-  b: ref(1)
-}
-expectType<Ref<number>>(toRef(obj, 'a'))
-expectType<Ref<number>>(toRef(obj, 'b'))
+// toRef and toRefs
+{
+  const obj: {
+    a: number
+    b: Ref<number>
+    c: number | string
+  } = {
+    a: 1,
+    b: ref(1),
+    c: 1
+  }
 
-const objWithUnionProp: { a: string | number } = {
-  a: 1
-}
+  // toRef
+  expectType<Ref<number>>(toRef(obj, 'a'))
+  expectType<Ref<number>>(toRef(obj, 'b'))
+  // Should not distribute Refs over union
+  expectType<Ref<number | string>>(toRef(obj, 'c'))
+
+  // toRefs
+  expectType<{
+    a: Ref<number>
+    b: Ref<number>
+    // Should not distribute Refs over union
+    c: Ref<number | string>
+  }>(toRefs(obj))
+
+  // Both should not do any unwrapping
+  const someReactive = shallowReactive({
+    a: {
+      b: ref(42)
+    }
+  })
 
-watch(toRef(objWithUnionProp, 'a'), value => {
-  expectType<string | number>(value)
-})
+  const toRefResult = toRef(someReactive, 'a')
+  const toRefsResult = toRefs(someReactive)
 
-// toRefs
-const objRefs = toRefs(obj)
-expectType<{
-  a: Ref<number>
-  b: Ref<number>
-}>(objRefs)
+  expectType<Ref<number>>(toRefResult.value.b)
+  expectType<Ref<number>>(toRefsResult.a.value.b)
+}
 
 // #2687
 interface AppData {
@@ -238,20 +254,6 @@ function testUnrefGenerics<T>(p: T | Ref<T>) {
 
 testUnrefGenerics(1)
 
-// #4732
-describe('ref in shallow reactive', () => {
-  const baz = shallowReactive({
-    foo: {
-      bar: ref(42)
-    }
-  })
-
-  const foo = toRef(baz, 'foo')
-
-  expectType<Ref<number>>(foo.value.bar)
-  expectType<number>(foo.value.bar.value)
-})
-
 // #4771
 describe('shallow reactive in reactive', () => {
   const baz = reactive({