Add serializer(vararg KSerializer<*>) override from SerializerFactory when necessary

SerializerFactory is an implementation detail for Kotlin/JS and Native:
it should be added as a supertype to a companion object of certain serializable classes
and `serializer(vararg KSerializer<*>)` function from it should be implemented.
Existing implementation added the supertype, but did not add proper override to FirClass
of a companion, which led to various warnings and errors like 'Abstract function 'serializer' is not implemented in non-abstract companion object'.

Also implemented the addition of SerializerFactory supertype to user-defined companions within @Serializable and @MetaSerializable when necessary.

Also set up proper box tests for FIR+Kotlin/JS combination.

#KT-58501 Fixed
#KT-59768 Fixed
diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/SerializationFirResolveExtension.kt b/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/SerializationFirResolveExtension.kt
index 6c4ffb7..800c50e 100644
--- a/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/SerializationFirResolveExtension.kt
+++ b/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/SerializationFirResolveExtension.kt
@@ -13,6 +13,7 @@
 import org.jetbrains.kotlin.fir.containingClassForStaticMemberAttr
 import org.jetbrains.kotlin.fir.copy
 import org.jetbrains.kotlin.fir.declarations.FirDeclarationOrigin
+import org.jetbrains.kotlin.fir.declarations.FirSimpleFunction
 import org.jetbrains.kotlin.fir.declarations.builder.buildSimpleFunctionCopy
 import org.jetbrains.kotlin.fir.declarations.origin
 import org.jetbrains.kotlin.fir.declarations.utils.isCompanion
@@ -28,17 +29,12 @@
 import org.jetbrains.kotlin.fir.scopes.scopeForSupertype
 import org.jetbrains.kotlin.fir.symbols.SymbolInternals
 import org.jetbrains.kotlin.fir.symbols.impl.*
-import org.jetbrains.kotlin.fir.types.ConeTypeProjection
-import org.jetbrains.kotlin.fir.types.classId
-import org.jetbrains.kotlin.fir.types.constructClassLikeType
-import org.jetbrains.kotlin.fir.types.constructType
+import org.jetbrains.kotlin.fir.types.*
 import org.jetbrains.kotlin.name.CallableId
 import org.jetbrains.kotlin.name.ClassId
 import org.jetbrains.kotlin.name.Name
 import org.jetbrains.kotlin.name.SpecialNames
-import org.jetbrains.kotlin.platform.isJs
-import org.jetbrains.kotlin.platform.isWasm
-import org.jetbrains.kotlin.platform.konan.isNative
+import org.jetbrains.kotlin.utils.addToStdlib.runIf
 import org.jetbrains.kotlinx.serialization.compiler.resolve.SerialEntityNames
 import org.jetbrains.kotlinx.serialization.compiler.resolve.SerialEntityNames.SERIALIZER_FACTORY_INTERFACE_NAME
 import org.jetbrains.kotlinx.serialization.compiler.resolve.SerializationPackages
@@ -151,7 +147,7 @@
             useSiteSuperType.scopeForSupertype(session, scopeSession, owner.fir, memberRequiredPhase = null)
         }
         val targets = scopes.flatMap { extractor(it) }
-        return targets.singleOrNull() ?: error("Multiple overrides found for ${callableId.callableName}")
+        return targets.singleOrNull() ?: error("Zero or multiple overrides found for ${callableId.callableName} in $owner")
     }
 
     @OptIn(SymbolInternals::class)
@@ -165,8 +161,13 @@
                 if (with(session) { owner.isSerializableObject }) owner else null
             }
 
-            serializableClass ?: return emptyList()
-            return listOf(generateSerializerGetterInCompanion(owner, serializableClass, callableId))
+            if (serializableClass == null) return emptyList()
+            val serializableGetterInCompanion = generateSerializerGetterInCompanion(owner, serializableClass, callableId)
+            val serializableGetterFromFactory = runIf (with(session) { serializableClass.companionNeedsSerializerFactory }) {
+                val original = getFromSupertype(callableId, owner) { it.getFunctions(callableId.callableName) }.fir
+                generateSerializerFactoryVararg(owner, callableId, original)
+            }
+            return listOfNotNull(serializableGetterInCompanion, serializableGetterFromFactory)
         }
         if (!owner.isSerializer) return emptyList()
         if (callableId.callableName !in setOf(
@@ -188,6 +189,18 @@
         return listOf(copy.symbol)
     }
 
+    private fun generateSerializerFactoryVararg(
+        owner: FirClassSymbol<*>,
+        callableId: CallableId,
+        original: FirSimpleFunction
+    ): FirNamedFunctionSymbol =
+        createMemberFunction(owner, SerializationPluginKey, callableId.callableName, original.returnTypeRef.coneType) {
+            val vpo = original.valueParameters.single()
+            valueParameter(vpo.name, vpo.returnTypeRef.coneType, vpo.isCrossinline, vpo.isNoinline, vpo.isVararg, vpo.defaultValue != null)
+        }.apply {
+            excludeFromJsExport()
+        }.symbol
+
     private fun generateSerializerGetterInCompanion(
         owner: FirClassSymbol<*>,
         serializableClassSymbol: FirClassSymbol<*>,
@@ -309,16 +322,4 @@
     private val FirClassSymbol<*>.isExternalSerializer: Boolean
         get() = session.predicateBasedProvider.matches(FirSerializationPredicates.serializerFor, this)
 
-    context(FirSession)
-    private val FirClassSymbol<*>.companionNeedsSerializerFactory: Boolean
-        get() {
-            if (!(moduleData.platform.isNative() || moduleData.platform.isJs() || moduleData.platform.isWasm())) return false
-            if (isSerializableObject) return true
-            if (isSerializableEnum) return true
-            if (isAbstractOrSealedSerializableClass) return true
-            if (isSealedSerializableInterface) return true
-            if (typeParameterSymbols.isEmpty()) return false
-            return true
-        }
-
 }
diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/SerializationFirSupertypesExtension.kt b/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/SerializationFirSupertypesExtension.kt
index 7cf4406..7343c8f 100644
--- a/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/SerializationFirSupertypesExtension.kt
+++ b/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/SerializationFirSupertypesExtension.kt
@@ -5,8 +5,13 @@
 
 package org.jetbrains.kotlinx.serialization.compiler.fir
 
+import org.jetbrains.kotlin.descriptors.isObject
 import org.jetbrains.kotlin.fir.FirSession
+import org.jetbrains.kotlin.fir.analysis.checkers.getContainingDeclarationSymbol
+import org.jetbrains.kotlin.fir.declarations.FirClass
 import org.jetbrains.kotlin.fir.declarations.FirClassLikeDeclaration
+import org.jetbrains.kotlin.fir.declarations.FirRegularClass
+import org.jetbrains.kotlin.fir.declarations.utils.isCompanion
 import org.jetbrains.kotlin.fir.expressions.FirExpression
 import org.jetbrains.kotlin.fir.expressions.FirGetClassCall
 import org.jetbrains.kotlin.fir.expressions.FirPropertyAccessExpression
@@ -14,22 +19,42 @@
 import org.jetbrains.kotlin.fir.extensions.FirSupertypeGenerationExtension
 import org.jetbrains.kotlin.fir.extensions.buildUserTypeFromQualifierParts
 import org.jetbrains.kotlin.fir.extensions.predicateBasedProvider
+import org.jetbrains.kotlin.fir.moduleData
 import org.jetbrains.kotlin.fir.references.impl.FirSimpleNamedReference
-import org.jetbrains.kotlin.fir.types.ConeKotlinType
-import org.jetbrains.kotlin.fir.types.FirResolvedTypeRef
+import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol
+import org.jetbrains.kotlin.fir.types.*
 import org.jetbrains.kotlin.fir.types.builder.buildResolvedTypeRef
-import org.jetbrains.kotlin.fir.types.classId
-import org.jetbrains.kotlin.fir.types.constructClassLikeType
 import org.jetbrains.kotlin.name.ClassId
 import org.jetbrains.kotlin.name.Name
+import org.jetbrains.kotlin.platform.isJs
+import org.jetbrains.kotlin.platform.isWasm
+import org.jetbrains.kotlin.platform.konan.isNative
+import org.jetbrains.kotlinx.serialization.compiler.fir.FirSerializationPredicates.annotatedWithSerializableOrMeta
 import org.jetbrains.kotlinx.serialization.compiler.fir.FirSerializationPredicates.serializerFor
 import org.jetbrains.kotlinx.serialization.compiler.resolve.SerialEntityNames
 import org.jetbrains.kotlinx.serialization.compiler.resolve.SerializationPackages
 
 class SerializationFirSupertypesExtension(session: FirSession) : FirSupertypeGenerationExtension(session) {
 
+    private val isJvmOrMetadata = !session.moduleData.platform.run { isNative() || isJs() || isWasm() }
+
     override fun needTransformSupertypes(declaration: FirClassLikeDeclaration): Boolean =
-        session.predicateBasedProvider.matches(serializerFor, declaration)
+        session.predicateBasedProvider.matches(serializerFor, declaration) || isSerializableObjectAndNeedsFactory(declaration) || isCompanionAndNeedsFactory(declaration)
+
+    private fun isSerializableObjectAndNeedsFactory(declaration: FirClassLikeDeclaration): Boolean = with(session) {
+        if (isJvmOrMetadata) return false
+        return declaration is FirClass && declaration.classKind.isObject
+                && session.predicateBasedProvider.matches(annotatedWithSerializableOrMeta, declaration)
+    }
+
+    private fun isCompanionAndNeedsFactory(declaration: FirClassLikeDeclaration): Boolean = with(session) {
+        if (isJvmOrMetadata) return false
+        if (declaration !is FirRegularClass) return false
+        if (!declaration.isCompanion) return false
+        val parentSymbol = declaration.symbol.getContainingDeclarationSymbol(session) as FirClassSymbol<*>
+        return session.predicateBasedProvider.matches(annotatedWithSerializableOrMeta, parentSymbol)
+                && parentSymbol.companionNeedsSerializerFactory
+    }
 
     override fun FirDeclarationPredicateRegistrar.registerPredicates() {
         register(serializerFor)
@@ -38,22 +63,33 @@
     context(TypeResolveServiceContainer)
     override fun computeAdditionalSupertypes(
         classLikeDeclaration: FirClassLikeDeclaration,
-        resolvedSupertypes: List<FirResolvedTypeRef>
+        resolvedSupertypes: List<FirResolvedTypeRef>,
     ): List<FirResolvedTypeRef> {
         val kSerializerClassId = ClassId(SerializationPackages.packageFqName, SerialEntityNames.KSERIALIZER_NAME)
         val generatedSerializerClassId = ClassId(SerializationPackages.internalPackageFqName, SerialEntityNames.GENERATED_SERIALIZER_CLASS)
-        if (resolvedSupertypes.any { it.type.classId == kSerializerClassId || it.type.classId == generatedSerializerClassId }) return emptyList()
 
-        return if (session.predicateBasedProvider.matches(serializerFor, classLikeDeclaration)) {
-            val getClassArgument = classLikeDeclaration.getSerializerFor(session) ?: return emptyList()
-            val serializerConeType = resolveConeTypeFromArgument(getClassArgument)
+        return when {
+            session.predicateBasedProvider.matches(serializerFor, classLikeDeclaration) -> {
+                if (resolvedSupertypes.any { it.type.classId == kSerializerClassId || it.type.classId == generatedSerializerClassId }) return emptyList()
+                val getClassArgument = classLikeDeclaration.getSerializerFor(session) ?: return emptyList()
+                val serializerConeType = resolveConeTypeFromArgument(getClassArgument)
 
-            listOf(
-                buildResolvedTypeRef {
-                    type = kSerializerClassId.constructClassLikeType(arrayOf(serializerConeType), isNullable = false)
-                }
-            )
-        } else emptyList()
+                listOf(
+                    buildResolvedTypeRef {
+                        type = kSerializerClassId.constructClassLikeType(arrayOf(serializerConeType), isNullable = false)
+                    }
+                )
+            }
+            isSerializableObjectAndNeedsFactory(classLikeDeclaration) || isCompanionAndNeedsFactory(classLikeDeclaration) -> {
+                val serializerFactoryClassId = ClassId(
+                    SerializationPackages.internalPackageFqName,
+                    SerialEntityNames.SERIALIZER_FACTORY_INTERFACE_NAME
+                )
+                if (resolvedSupertypes.any { it.type.classId == serializerFactoryClassId }) return emptyList()
+                listOf(serializerFactoryClassId.constructClassLikeType(emptyArray(), false).toFirResolvedTypeRef())
+            }
+            else -> emptyList()
+        }
     }
 
     // Function helps to resolve class call from annotation argument to `ConeKotlinType`
diff --git a/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/SerializationFirUtils.kt b/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/SerializationFirUtils.kt
index 031c20d..fb01207 100644
--- a/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/SerializationFirUtils.kt
+++ b/plugins/kotlinx-serialization/kotlinx-serialization.k2/src/org/jetbrains/kotlinx/serialization/compiler/fir/SerializationFirUtils.kt
@@ -29,6 +29,8 @@
 import org.jetbrains.kotlin.fir.types.builder.buildResolvedTypeRef
 import org.jetbrains.kotlin.name.Name
 import org.jetbrains.kotlin.platform.isJs
+import org.jetbrains.kotlin.platform.isWasm
+import org.jetbrains.kotlin.platform.konan.isNative
 import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
 import org.jetbrains.kotlinx.serialization.compiler.fir.services.dependencySerializationInfoProvider
 import org.jetbrains.kotlinx.serialization.compiler.resolve.SerialEntityNames
@@ -146,6 +148,18 @@
             || isSealedSerializableInterface
 
 context(FirSession)
+internal val FirClassSymbol<*>.companionNeedsSerializerFactory: Boolean
+    get() {
+        if (!moduleData.platform.run { isNative() || isJs() || isWasm() }) return false
+        if (isSerializableObject) return true
+        if (isSerializableEnum) return true
+        if (isAbstractOrSealedSerializableClass) return true
+        if (isSealedSerializableInterface) return true
+        if (typeParameterSymbols.isEmpty()) return false
+        return true
+    }
+
+context(FirSession)
 internal val FirClassSymbol<*>.isInternalSerializable: Boolean
     get() {
         if (!classKind.isClass) return false
diff --git a/plugins/kotlinx-serialization/testData/boxIr/serializerFactory.kt b/plugins/kotlinx-serialization/testData/boxIr/serializerFactory.kt
new file mode 100644
index 0000000..45f2b34
--- /dev/null
+++ b/plugins/kotlinx-serialization/testData/boxIr/serializerFactory.kt
@@ -0,0 +1,26 @@
+// WITH_STDLIB
+// ISSUE: KT-58501
+
+import kotlinx.serialization.*
+import kotlinx.serialization.json.*
+
+@Serializable object Objekt
+
+@Serializable sealed class SealedInterface
+@Serializable data object Inheritor: SealedInterface()
+
+@Serializable enum class EnumKlass { INSTANCE }
+
+@Serializable class Plain
+
+fun box(): String {
+    serializer<Objekt>()
+    Objekt.serializer()
+    serializer<EnumKlass>()
+    EnumKlass.serializer()
+    serializer<SealedInterface>()
+    SealedInterface.serializer()
+    serializer<Plain>()
+    Plain.serializer()
+    return "OK"
+}
diff --git a/plugins/kotlinx-serialization/testData/boxIr/serializerFactoryInUserDefined.kt b/plugins/kotlinx-serialization/testData/boxIr/serializerFactoryInUserDefined.kt
new file mode 100644
index 0000000..a0a3590
--- /dev/null
+++ b/plugins/kotlinx-serialization/testData/boxIr/serializerFactoryInUserDefined.kt
@@ -0,0 +1,45 @@
+// WITH_STDLIB
+// ISSUE: KT-59768
+
+import kotlinx.serialization.*
+import kotlinx.serialization.json.*
+
+@Serializable sealed class SealedInterface {
+    companion object {
+        fun unrelated() {}
+    }
+}
+@Serializable enum class EnumKlass { INSTANCE;
+    companion object  {
+        fun unrelated() {}
+    }
+}
+
+@Serializable class Plain {
+    companion object  {
+        fun unrelated() {}
+    }
+}
+
+@MetaSerializable
+@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
+annotation class MySerializable
+
+@MySerializable
+sealed interface SealedMeta {
+    companion object  {
+        fun unrelated() {}
+    }
+}
+
+fun box(): String {
+    serializer<EnumKlass>()
+    EnumKlass.serializer()
+    serializer<SealedInterface>()
+    SealedInterface.serializer()
+    serializer<Plain>()
+    Plain.serializer()
+    serializer<SealedMeta>()
+    SealedMeta.serializer()
+    return "OK"
+}
diff --git a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirJsBoxTestGenerated.java b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirJsBoxTestGenerated.java
new file mode 100644
index 0000000..acf4eae
--- /dev/null
+++ b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirJsBoxTestGenerated.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlinx.serialization.runners;
+
+import com.intellij.testFramework.TestDataPath;
+import org.jetbrains.kotlin.test.util.KtTestUtil;
+import org.jetbrains.kotlin.test.TargetBackend;
+import org.jetbrains.kotlin.test.TestMetadata;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.util.regex.Pattern;
+
+/** This class is generated by {@link org.jetbrains.kotlinx.serialization.TestGeneratorKt}. DO NOT MODIFY MANUALLY */
+@SuppressWarnings("all")
+@TestMetadata("plugins/kotlinx-serialization/testData/boxIr")
+@TestDataPath("$PROJECT_ROOT")
+public class SerializationFirJsBoxTestGenerated extends AbstractSerializationFirJsBoxTest {
+    @Test
+    public void testAllFilesPresentInBoxIr() throws Exception {
+        KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("plugins/kotlinx-serialization/testData/boxIr"), Pattern.compile("^(.+)\\.kt$"), null, TargetBackend.JS_IR, true);
+    }
+
+    @Test
+    @TestMetadata("constValInSerialName.kt")
+    public void testConstValInSerialName() throws Exception {
+        runTest("plugins/kotlinx-serialization/testData/boxIr/constValInSerialName.kt");
+    }
+
+    @Test
+    @TestMetadata("delegatedProperty.kt")
+    public void testDelegatedProperty() throws Exception {
+        runTest("plugins/kotlinx-serialization/testData/boxIr/delegatedProperty.kt");
+    }
+
+    @Test
+    @TestMetadata("excludedFromExport.kt")
+    public void testExcludedFromExport() throws Exception {
+        runTest("plugins/kotlinx-serialization/testData/boxIr/excludedFromExport.kt");
+    }
+
+    @Test
+    @TestMetadata("excludedFromFileExport.kt")
+    public void testExcludedFromFileExport() throws Exception {
+        runTest("plugins/kotlinx-serialization/testData/boxIr/excludedFromFileExport.kt");
+    }
+
+    @Test
+    @TestMetadata("serializerFactory.kt")
+    public void testSerializerFactory() throws Exception {
+        runTest("plugins/kotlinx-serialization/testData/boxIr/serializerFactory.kt");
+    }
+
+    @Test
+    @TestMetadata("serializerFactoryInUserDefined.kt")
+    public void testSerializerFactoryInUserDefined() throws Exception {
+        runTest("plugins/kotlinx-serialization/testData/boxIr/serializerFactoryInUserDefined.kt");
+    }
+}
diff --git a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirLightTreeBlackBoxTestGenerated.java b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirLightTreeBlackBoxTestGenerated.java
index 998435a..2f5a114 100644
--- a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirLightTreeBlackBoxTestGenerated.java
+++ b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationFirLightTreeBlackBoxTestGenerated.java
@@ -232,6 +232,18 @@
         }
 
         @Test
+        @TestMetadata("serializerFactory.kt")
+        public void testSerializerFactory() throws Exception {
+            runTest("plugins/kotlinx-serialization/testData/boxIr/serializerFactory.kt");
+        }
+
+        @Test
+        @TestMetadata("serializerFactoryInUserDefined.kt")
+        public void testSerializerFactoryInUserDefined() throws Exception {
+            runTest("plugins/kotlinx-serialization/testData/boxIr/serializerFactoryInUserDefined.kt");
+        }
+
+        @Test
         @TestMetadata("starProjections.kt")
         public void testStarProjections() throws Exception {
             runTest("plugins/kotlinx-serialization/testData/boxIr/starProjections.kt");
diff --git a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrBoxTestGenerated.java b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrBoxTestGenerated.java
index 30ed8b0..d9fef50 100644
--- a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrBoxTestGenerated.java
+++ b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrBoxTestGenerated.java
@@ -230,6 +230,18 @@
     }
 
     @Test
+    @TestMetadata("serializerFactory.kt")
+    public void testSerializerFactory() throws Exception {
+        runTest("plugins/kotlinx-serialization/testData/boxIr/serializerFactory.kt");
+    }
+
+    @Test
+    @TestMetadata("serializerFactoryInUserDefined.kt")
+    public void testSerializerFactoryInUserDefined() throws Exception {
+        runTest("plugins/kotlinx-serialization/testData/boxIr/serializerFactoryInUserDefined.kt");
+    }
+
+    @Test
     @TestMetadata("starProjections.kt")
     public void testStarProjections() throws Exception {
         runTest("plugins/kotlinx-serialization/testData/boxIr/starProjections.kt");
diff --git a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrJsBoxTestGenerated.java b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrJsBoxTestGenerated.java
index 17635da..d9e2e2f 100644
--- a/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrJsBoxTestGenerated.java
+++ b/plugins/kotlinx-serialization/tests-gen/org/jetbrains/kotlinx/serialization/runners/SerializationIrJsBoxTestGenerated.java
@@ -48,4 +48,16 @@
     public void testExcludedFromFileExport() throws Exception {
         runTest("plugins/kotlinx-serialization/testData/boxIr/excludedFromFileExport.kt");
     }
+
+    @Test
+    @TestMetadata("serializerFactory.kt")
+    public void testSerializerFactory() throws Exception {
+        runTest("plugins/kotlinx-serialization/testData/boxIr/serializerFactory.kt");
+    }
+
+    @Test
+    @TestMetadata("serializerFactoryInUserDefined.kt")
+    public void testSerializerFactoryInUserDefined() throws Exception {
+        runTest("plugins/kotlinx-serialization/testData/boxIr/serializerFactoryInUserDefined.kt");
+    }
 }
diff --git a/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/TestGenerator.kt b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/TestGenerator.kt
index 14768d7..d70f543 100644
--- a/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/TestGenerator.kt
+++ b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/TestGenerator.kt
@@ -64,6 +64,10 @@
             testClass<AbstractSerializationIrJsBoxTest> {
                 model("boxIr")
             }
+
+            testClass<AbstractSerializationFirJsBoxTest> {
+                model("boxIr")
+            }
         }
     }
 }
diff --git a/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/runners/BoxTests.kt b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/runners/BoxTests.kt
index 47812a8..6c66f68 100644
--- a/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/runners/BoxTests.kt
+++ b/plugins/kotlinx-serialization/tests/org/jetbrains/kotlinx/serialization/runners/BoxTests.kt
@@ -5,6 +5,8 @@
 
 package org.jetbrains.kotlinx.serialization.runners
 
+import org.jetbrains.kotlin.js.test.fir.AbstractFirJsBoxTest
+import org.jetbrains.kotlin.js.test.fir.AbstractFirJsTest
 import org.jetbrains.kotlin.test.builders.TestConfigurationBuilder
 import org.jetbrains.kotlin.test.runners.codegen.AbstractFirLightTreeBlackBoxCodegenTest
 import org.jetbrains.kotlin.test.runners.codegen.AbstractIrBlackBoxCodegenTest
@@ -48,4 +50,14 @@
         super.configure(builder)
         builder.configureForKotlinxSerialization(target = TargetBackend.JS_IR)
     }
-}
\ No newline at end of file
+}
+
+open class AbstractSerializationFirJsBoxTest : AbstractFirJsTest(
+    pathToTestDir = "plugins/kotlinx-serialization/testData/boxIr/",
+    testGroupOutputDirPrefix = "codegen/serializationBoxFir/"
+) {
+    override fun configure(builder: TestConfigurationBuilder) {
+        super.configure(builder)
+        builder.configureForKotlinxSerialization(target = TargetBackend.JS_IR)
+    }
+}