From ac7fd9eeeda9a258f35a5f3cb68db2a43968086d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunnar=20M=C3=A1r=20=C3=93ttarsson?= Date: Fri, 12 May 2017 22:52:31 +0000 Subject: [PATCH 1/9] React.Router initial draft --- build.proj | 9 +- src/React.Core/ReactEnvironment.cs | 17 ++ src/React.Router/ExecutionResult.cs | 19 +++ src/React.Router/HtmlHelperExtensions.cs | 155 ++++++++++++++++++ src/React.Router/Properties/AssemblyInfo.cs | 7 + src/React.Router/React.Router.csproj | 48 ++++++ .../ReactEnvironmentExtensions.cs | 47 ++++++ src/React.Router/ReactRouterComponent.cs | 107 ++++++++++++ src/React.Router/ReactRouterException.cs | 41 +++++ src/React.Router/RoutingContext.cs | 20 +++ src/React.sln | 7 + tests/React.Tests/React.Tests.csproj | 4 + 12 files changed, 477 insertions(+), 4 deletions(-) create mode 100644 src/React.Router/ExecutionResult.cs create mode 100644 src/React.Router/HtmlHelperExtensions.cs create mode 100644 src/React.Router/Properties/AssemblyInfo.cs create mode 100644 src/React.Router/React.Router.csproj create mode 100644 src/React.Router/ReactEnvironmentExtensions.cs create mode 100644 src/React.Router/ReactRouterComponent.cs create mode 100644 src/React.Router/ReactRouterException.cs create mode 100644 src/React.Router/RoutingContext.cs diff --git a/build.proj b/build.proj index dd156a7ed..165c28b9d 100644 --- a/build.proj +++ b/build.proj @@ -10,8 +10,8 @@ of patent rights can be found in the PATENTS file in the same directory. 3 - 0 - 1 + 1 + 0 0 http://reactjs.net/packages/ $(MSBuildProjectDirectory)\tools\MSBuildTasks @@ -26,8 +26,9 @@ of patent rights can be found in the PATENTS file in the same directory. - - + + + diff --git a/src/React.Core/ReactEnvironment.cs b/src/React.Core/ReactEnvironment.cs index c6decd161..70d3b9716 100644 --- a/src/React.Core/ReactEnvironment.cs +++ b/src/React.Core/ReactEnvironment.cs @@ -273,6 +273,23 @@ public virtual IReactComponent CreateComponent(string componentName, T props, return component; } + /// + /// Adds the provided to the list of components to render client side. + /// + /// Component to add to client side render list + /// True if server-side rendering will be bypassed. Defaults to false. + /// The component + public virtual IReactComponent CreateComponent(IReactComponent component, bool clientOnly = false) + { + if (!clientOnly) + { + EnsureUserScriptsLoaded(); + } + + _components.Add(component); + return component; + } + /// /// Renders the JavaScript required to initialise all components client-side. This will /// attach event handlers to the server-rendered HTML. diff --git a/src/React.Router/ExecutionResult.cs b/src/React.Router/ExecutionResult.cs new file mode 100644 index 000000000..ca5f8d08d --- /dev/null +++ b/src/React.Router/ExecutionResult.cs @@ -0,0 +1,19 @@ +namespace React.Router +{ + /// + /// Contains the context object used during execution in addition to + /// the string result of rendering the React Router component. + /// + public class ExecutionResult + { + /// + /// String result of ReactDOMServer render of provided component. + /// + public string renderResult { get; set; } + + /// + /// Context object used during JS engine execution. + /// + public RoutingContext context { get; set; } + } +} diff --git a/src/React.Router/HtmlHelperExtensions.cs b/src/React.Router/HtmlHelperExtensions.cs new file mode 100644 index 000000000..21b94a2ca --- /dev/null +++ b/src/React.Router/HtmlHelperExtensions.cs @@ -0,0 +1,155 @@ +using System; +using React.Exceptions; +using React.TinyIoC; + +#if NET451 +using System.Web; +using System.Web.Mvc; +using HttpResponse = System.Web.HttpResponseBase; +using IHtmlHelper = System.Web.Mvc.HtmlHelper; +#else +using Microsoft.AspNetCore.Mvc.Rendering; +using IHtmlString = Microsoft.AspNetCore.Html.IHtmlContent; +using HttpResponse = Microsoft.AspNetCore.Http.HttpResponse; +using Microsoft.AspNetCore.Html; +#endif + +namespace React.Router +{ + /// + /// Render a React StaticRouter Component with context. + /// + public static class HtmlHelperExtensions + { + /// + /// Gets the React environment + /// + private static IReactEnvironment Environment + { + get + { + try + { + return ReactEnvironment.Current; + } + catch (TinyIoCResolutionException ex) + { + throw new ReactNotInitialisedException( +#if NET451 + "ReactJS.NET has not been initialised correctly.", +#else + "ReactJS.NET has not been initialised correctly. Please ensure you have " + + "called services.AddReact() and app.UseReact() in your Startup.cs file.", +#endif + ex + ); + } + } + } + + /// + /// Render a React StaticRouter Component with context object. + /// Can optionally be provided with a custom context handler to handle the various status codes. + /// + /// + /// MVC Razor + /// Name of React Static Router component. Expose component globally to ReactJS.NET + /// Props to initialise the component with + /// F.x. from Request.Url.AbsolutePath. Used by React Static Router to determine context and routing. + /// Used either by contextHandler or internally to modify the Response status code and redirect. + /// Optional custom context handler, can be used instead of providing a Response object + /// HTML tag to wrap the component in. Defaults to <div> + /// ID to use for the container HTML tag. Defaults to an auto-generated ID + /// Skip rendering server-side and only output client-side initialisation code. Defaults to false + /// Skip rendering React specific data-attributes during server side rendering. Defaults to false + /// HTML class(es) to set on the container tag + /// containing the rendered markup for provided React Router component + public static IHtmlString ReactRouterWithContext( + this IHtmlHelper htmlHelper, + string componentName, + T props, + string path = null, + HttpResponse Response = null, + string htmlTag = null, + string containerId = null, + bool clientOnly = false, + bool serverOnly = false, + string containerClass = null, + Action contextHandler = null + ) + { + try + { + path = path ?? htmlHelper.ViewContext.HttpContext.Request.Path; + Response = Response ?? htmlHelper.ViewContext.HttpContext.Response; + + var reactComponent = Environment.CreateRouterComponent(componentName, props, containerId, clientOnly); + if (!string.IsNullOrEmpty(htmlTag)) + { + reactComponent.ContainerTag = htmlTag; + } + if (!string.IsNullOrEmpty(containerClass)) + { + reactComponent.ContainerClass = containerClass; + } + + var executionResult = reactComponent.RenderRouterWithContext(path, clientOnly, serverOnly); + + if (executionResult.context?.status != null) + { + // Use provided contextHandler + if (contextHandler != null) + { + contextHandler(Response, executionResult.context); + } + // Handle routing context internally + else + { + HandleRoutingContext(executionResult.context, Response); + } + } + + return new HtmlString(executionResult.renderResult); + } + finally + { + Environment.ReturnEngineToPool(); + } + } + + private static void HandleRoutingContext(RoutingContext context, HttpResponse Response) + { + var statusCode = context.status.Value; + + // 300-399 + if (statusCode >= 300 && statusCode < 400) + { + if (!string.IsNullOrEmpty(context.url)) + { + if (statusCode == 301) + { + +#if NET451 + + Response.RedirectPermanent(context.url); +#else + Response.Redirect(context.url, true); +#endif + } + else // 302 and all others + { + Response.Redirect(context.url); + } + } + else + { + throw new ReactRouterException("Router requested redirect but no url provided."); + } + } + else + { + Response.StatusCode = statusCode; + } + } + } +} diff --git a/src/React.Router/Properties/AssemblyInfo.cs b/src/React.Router/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..c2cbf2eae --- /dev/null +++ b/src/React.Router/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("React.Router")] +[assembly: AssemblyDescription("React Router support for ReactJS.NET")] +[assembly: ComVisible(false)] +[assembly: Guid("277850fc-8765-4042-945f-a50b8f2525a9")] diff --git a/src/React.Router/React.Router.csproj b/src/React.Router/React.Router.csproj new file mode 100644 index 000000000..5f419cef8 --- /dev/null +++ b/src/React.Router/React.Router.csproj @@ -0,0 +1,48 @@ + + + + React Router support for ReactJS.NET. + Copyright 2014-Present Facebook, Inc + ReactJS.NET Router + Daniel Lo Nigro, Gunnar Már Óttarsson + net451;netstandard1.6 + true + React.Router + ../key.snk + true + true + React.Router + asp.net;mvc;asp;javascript;js;react;facebook;reactjs;babel + http://reactjs.net/img/logo_64.png + http://reactjs.net/ + https://github.com/reactjs/React.NET#licence + false + + + + TRACE;DEBUG;ASPNETCORE;NET451 + + + + + + + + + + + + + + + + + + + + + + true + + + diff --git a/src/React.Router/ReactEnvironmentExtensions.cs b/src/React.Router/ReactEnvironmentExtensions.cs new file mode 100644 index 000000000..5b14a2ca2 --- /dev/null +++ b/src/React.Router/ReactEnvironmentExtensions.cs @@ -0,0 +1,47 @@ +namespace React.Router +{ + /// + /// extension for rendering a React Router Component with context + /// + public static class ReactEnvironmentExtensions + { + /// + /// Create a React Router Component with context and add it to the list of components to render client side, + /// if applicable. + /// + /// Type of the props + /// React Environment + /// Name of the component + /// Props to use + /// ID to use for the container HTML tag. Defaults to an auto-generated ID + /// True if server-side rendering will be bypassed. Defaults to false. + /// + public static ReactRouterComponent CreateRouterComponent( + this IReactEnvironment env, + string componentName, + T props, + string containerId = null, + bool clientOnly = false + ) + { + var config = React.AssemblyRegistration.Container.Resolve(); + + var component = new ReactRouterComponent(env, config, componentName, containerId) + { + Props = props, + }; + + var reactEnvironment = env as ReactEnvironment; + + if (reactEnvironment != null) + { + reactEnvironment.CreateComponent(component, clientOnly); + return component; + } + else + { + throw new ReactRouterException("Only the default ReactEnvironment is intended to be used with React.Router"); + } + } + } +} diff --git a/src/React.Router/ReactRouterComponent.cs b/src/React.Router/ReactRouterComponent.cs new file mode 100644 index 000000000..bef362e35 --- /dev/null +++ b/src/React.Router/ReactRouterComponent.cs @@ -0,0 +1,107 @@ +using JavaScriptEngineSwitcher.Core; +using Newtonsoft.Json; +using React.Exceptions; + +namespace React.Router +{ + /// + /// Represents a React Router JavaScript component. + /// + public class ReactRouterComponent : ReactComponent + { + /// + /// Initializes a new instance of the class. + /// + /// The environment. + /// Site-wide configuration. + /// Name of the component. + /// The ID of the container DIV for this component + public ReactRouterComponent( + IReactEnvironment environment, + IReactSiteConfiguration configuration, + string componentName, + string containerId + ) : base(environment, configuration, componentName, containerId) + { + + } + + /// + /// Render a React StaticRouter Component with context object. + /// + /// Current request URL path. F.x. Request.URL.AbsolutePath + /// Only renders component container. Used for client-side only rendering. + /// Only renders the common HTML mark up and not any React specific data attributes. Used for server-side only rendering. + /// Object containing HTML in string format and the React Router context object + public ExecutionResult RenderRouterWithContext(string path, bool renderContainerOnly = false, bool renderServerOnly = false) + { + if (!_configuration.UseServerSideRendering) + { + renderContainerOnly = true; + } + + if (!renderContainerOnly) + { + EnsureComponentExists(); + } + + try + { + ExecutionResult executionResult; + + if (!renderContainerOnly) + { + var componentInitialiser = string.Format("React.createElement({0}, {{ path: '{1}', context: context }})", ComponentName, path ); + + var reactDomServerMethod = renderServerOnly ? "renderToStaticMarkup" : "renderToString"; + + var reactRenderCommand = string.Format(@" + + var context = {{}}; + + var renderResult = ReactDOMServer.{0}( + React.createElement({1}, {{ path: '{2}', context: context }}) + ); + + JSON.stringify({{ + renderResult: renderResult, + context: context + }}); + ", reactDomServerMethod, ComponentName, path); + + var strResult = _environment.Execute(reactRenderCommand); + executionResult = JsonConvert.DeserializeObject(strResult); + } + else + { + executionResult = new ExecutionResult(); + } + + string attributes = string.Format("id=\"{0}\"", ContainerId); + if (!string.IsNullOrEmpty(ContainerClass)) + { + attributes += string.Format(" class=\"{0}\"", ContainerClass); + } + + executionResult.renderResult = + string.Format( + "<{2} {0}>{1}", + attributes, + executionResult.renderResult, + ContainerTag + ); + + return executionResult; + } + catch (JsRuntimeException ex) + { + throw new ReactServerRenderingException(string.Format( + "Error while rendering \"{0}\" to \"{2}\": {1}", + ComponentName, + ex.Message, + ContainerId + )); + } + } + } +} diff --git a/src/React.Router/ReactRouterException.cs b/src/React.Router/ReactRouterException.cs new file mode 100644 index 000000000..5dde76974 --- /dev/null +++ b/src/React.Router/ReactRouterException.cs @@ -0,0 +1,41 @@ +using System; +using System.Runtime.Serialization; + +namespace React.Router +{ + /// + /// React Router Exception + /// +#if NET451 + [Serializable] +#endif + public class ReactRouterException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public ReactRouterException() : base() { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public ReactRouterException(string message) : base(message) { } + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if no inner exception is specified. + public ReactRouterException(string message, Exception innerException) + : base(message, innerException) { } + + +#if NET451 + /// + /// Used by deserialization + /// + protected ReactRouterException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } +#endif + } +} diff --git a/src/React.Router/RoutingContext.cs b/src/React.Router/RoutingContext.cs new file mode 100644 index 000000000..efb313ff6 --- /dev/null +++ b/src/React.Router/RoutingContext.cs @@ -0,0 +1,20 @@ +namespace React.Router +{ + /// + /// Context object used during render of React Router component + /// + public class RoutingContext + { + /// + /// HTTP Status Code. + /// If present signifies that the given status code should be returned by server. + /// + public int? status { get; set; } + + /// + /// URL to redirect to. + /// If included this signals that React Router determined a redirect should happen. + /// + public string url { get; set; } + } +} diff --git a/src/React.sln b/src/React.sln index 6533baa78..d5739dc96 100644 --- a/src/React.sln +++ b/src/React.sln @@ -63,6 +63,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "React.AspNet", "React.AspNe EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "React.Sample.Mvc6", "React.Sample.Mvc6\React.Sample.Mvc6.csproj", "{6E2C1144-703C-4FF5-893D-B9C1A2C55C3B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "React.Router", "React.Router\React.Router.csproj", "{D076273B-C5EA-47C7-923D-523E4C5EE30D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -129,6 +131,10 @@ Global {6E2C1144-703C-4FF5-893D-B9C1A2C55C3B}.Debug|Any CPU.Build.0 = Debug|Any CPU {6E2C1144-703C-4FF5-893D-B9C1A2C55C3B}.Release|Any CPU.ActiveCfg = Release|Any CPU {6E2C1144-703C-4FF5-893D-B9C1A2C55C3B}.Release|Any CPU.Build.0 = Release|Any CPU + {D076273B-C5EA-47C7-923D-523E4C5EE30D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D076273B-C5EA-47C7-923D-523E4C5EE30D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D076273B-C5EA-47C7-923D-523E4C5EE30D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D076273B-C5EA-47C7-923D-523E4C5EE30D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -149,5 +155,6 @@ Global {6AA0D75E-5797-4690-BEFC-098A60C511A3} = {F567B25C-E869-4C93-9C96-077761250F87} {631FCC55-0219-46DC-838A-C5A3E878943A} = {681C45FB-103C-48BC-B992-20C5B6B78F92} {6E2C1144-703C-4FF5-893D-B9C1A2C55C3B} = {A51CE5B6-294F-4D39-B32B-BF08DAF9B40B} + {D076273B-C5EA-47C7-923D-523E4C5EE30D} = {681C45FB-103C-48BC-B992-20C5B6B78F92} EndGlobalSection EndGlobal diff --git a/tests/React.Tests/React.Tests.csproj b/tests/React.Tests/React.Tests.csproj index 662774500..a6ed53d3c 100644 --- a/tests/React.Tests/React.Tests.csproj +++ b/tests/React.Tests/React.Tests.csproj @@ -39,4 +39,8 @@ + + + + From debe9b98d19d899fdea8031155624a2fdbd25f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunnar=20M=C3=A1r=20=C3=93ttarsson?= Date: Sat, 13 May 2017 01:16:41 +0000 Subject: [PATCH 2/9] spaces -> tabs --- src/React.Router/ReactRouterComponent.cs | 202 ++++++++++++----------- 1 file changed, 108 insertions(+), 94 deletions(-) diff --git a/src/React.Router/ReactRouterComponent.cs b/src/React.Router/ReactRouterComponent.cs index bef362e35..16e44fa3f 100644 --- a/src/React.Router/ReactRouterComponent.cs +++ b/src/React.Router/ReactRouterComponent.cs @@ -8,100 +8,114 @@ namespace React.Router /// Represents a React Router JavaScript component. /// public class ReactRouterComponent : ReactComponent - { - /// - /// Initializes a new instance of the class. - /// - /// The environment. - /// Site-wide configuration. - /// Name of the component. - /// The ID of the container DIV for this component - public ReactRouterComponent( - IReactEnvironment environment, - IReactSiteConfiguration configuration, - string componentName, + { + /// + /// Initializes a new instance of the class. + /// + /// The environment. + /// Site-wide configuration. + /// Name of the component. + /// The ID of the container DIV for this component + public ReactRouterComponent( + IReactEnvironment environment, + IReactSiteConfiguration configuration, + string componentName, string containerId ) : base(environment, configuration, componentName, containerId) - { - - } - - /// - /// Render a React StaticRouter Component with context object. - /// - /// Current request URL path. F.x. Request.URL.AbsolutePath - /// Only renders component container. Used for client-side only rendering. - /// Only renders the common HTML mark up and not any React specific data attributes. Used for server-side only rendering. - /// Object containing HTML in string format and the React Router context object - public ExecutionResult RenderRouterWithContext(string path, bool renderContainerOnly = false, bool renderServerOnly = false) - { - if (!_configuration.UseServerSideRendering) - { - renderContainerOnly = true; - } - - if (!renderContainerOnly) - { - EnsureComponentExists(); - } - - try - { - ExecutionResult executionResult; - - if (!renderContainerOnly) - { - var componentInitialiser = string.Format("React.createElement({0}, {{ path: '{1}', context: context }})", ComponentName, path ); - - var reactDomServerMethod = renderServerOnly ? "renderToStaticMarkup" : "renderToString"; - - var reactRenderCommand = string.Format(@" - - var context = {{}}; - - var renderResult = ReactDOMServer.{0}( - React.createElement({1}, {{ path: '{2}', context: context }}) - ); - - JSON.stringify({{ - renderResult: renderResult, - context: context - }}); - ", reactDomServerMethod, ComponentName, path); - - var strResult = _environment.Execute(reactRenderCommand); - executionResult = JsonConvert.DeserializeObject(strResult); - } - else - { - executionResult = new ExecutionResult(); - } - - string attributes = string.Format("id=\"{0}\"", ContainerId); - if (!string.IsNullOrEmpty(ContainerClass)) - { - attributes += string.Format(" class=\"{0}\"", ContainerClass); - } - - executionResult.renderResult = - string.Format( - "<{2} {0}>{1}", - attributes, - executionResult.renderResult, - ContainerTag - ); - - return executionResult; - } - catch (JsRuntimeException ex) - { - throw new ReactServerRenderingException(string.Format( - "Error while rendering \"{0}\" to \"{2}\": {1}", - ComponentName, - ex.Message, - ContainerId - )); - } - } - } + { + + } + + /// + /// 's do not use this method. + /// Instead use RenderRouterWithContext. + /// + /// + /// + /// + public override string RenderHtml(bool renderContainerOnly = false, bool renderServerOnly = false) + { + throw new ReactRouterException + (@"React Router Components are rendered with RenderRouterWithContext. + Please use ReactComponent if this functionality is not desired."); + } + + /// + /// Render a React StaticRouter Component with context object. + /// + /// Current request URL path. F.x. Request.Path + /// Only renders component container. Used for client-side only rendering. Does not make sense in this context but included for consistency + /// Only renders the common HTML mark up and not any React specific data attributes. Used for server-side only rendering. + /// Object containing HTML in string format and the React Router context object + public ExecutionResult RenderRouterWithContext(string path, bool renderContainerOnly = false, bool renderServerOnly = false) + { + if (!_configuration.UseServerSideRendering) + { + renderContainerOnly = true; + } + + if (!renderContainerOnly) + { + EnsureComponentExists(); + } + + try + { + ExecutionResult executionResult; + + if (!renderContainerOnly) + { + var componentInitialiser = string.Format("React.createElement({0}, {{ path: '{1}', context: context }})", ComponentName, path ); + + var reactDomServerMethod = renderServerOnly ? "renderToStaticMarkup" : "renderToString"; + + var reactRenderCommand = string.Format(@" + + var context = {{}}; + + var renderResult = ReactDOMServer.{0}( + React.createElement({1}, {{ path: '{2}', context: context }}) + ); + + JSON.stringify({{ + renderResult: renderResult, + context: context + }}); + ", reactDomServerMethod, ComponentName, path); + + var strResult = _environment.Execute(reactRenderCommand); + executionResult = JsonConvert.DeserializeObject(strResult); + } + else + { + executionResult = new ExecutionResult(); + } + + string attributes = string.Format("id=\"{0}\"", ContainerId); + if (!string.IsNullOrEmpty(ContainerClass)) + { + attributes += string.Format(" class=\"{0}\"", ContainerClass); + } + + executionResult.renderResult = + string.Format( + "<{2} {0}>{1}", + attributes, + executionResult.renderResult, + ContainerTag + ); + + return executionResult; + } + catch (JsRuntimeException ex) + { + throw new ReactServerRenderingException(string.Format( + "Error while rendering \"{0}\" to \"{2}\": {1}", + ComponentName, + ex.Message, + ContainerId + )); + } + } + } } From 81beb7cea75c4c4083dec9c5ef98cc2abebe2bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunnar=20M=C3=A1r=20=C3=93ttarsson?= Date: Sat, 13 May 2017 01:16:53 +0000 Subject: [PATCH 3/9] tests WIP --- tests/React.Tests/React.Tests.csproj | 5 +- .../Router/ReactRouterComponentTest.cs | 215 ++++++++++++++++++ 2 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 tests/React.Tests/Router/ReactRouterComponentTest.cs diff --git a/tests/React.Tests/React.Tests.csproj b/tests/React.Tests/React.Tests.csproj index a6ed53d3c..8f21419d2 100644 --- a/tests/React.Tests/React.Tests.csproj +++ b/tests/React.Tests/React.Tests.csproj @@ -18,6 +18,7 @@ + @@ -39,8 +40,4 @@ - - - - diff --git a/tests/React.Tests/Router/ReactRouterComponentTest.cs b/tests/React.Tests/Router/ReactRouterComponentTest.cs new file mode 100644 index 000000000..9bf26023c --- /dev/null +++ b/tests/React.Tests/Router/ReactRouterComponentTest.cs @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2014-Present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +using Moq; +using Xunit; +using React.Exceptions; +using React.Router; + +namespace React.Tests.Router +{ + public class ReactRouterComponentTest + { + [Fact] + public void RenderHtmlShouldThrowExceptionIfComponentDoesNotExist() + { + var environment = new Mock(); + environment.Setup(x => x.Execute("typeof Foo !== 'undefined'")).Returns(false); + var config = new Mock(); + config.Setup(x => x.UseServerSideRendering).Returns(true); + var component = new ReactComponent(environment.Object, config.Object, "Foo", "container"); + + Assert.Throws(() => + { + component.RenderHtml(); + }); + } + + [Fact] + public void blabla() + { + new Mock().Verify(x => x.ToString()); + } + + [Fact] + public void RenderHtmlShouldCallRenderComponent() + { + var environment = new Mock(); + environment.Setup(x => x.Execute("typeof Foo !== 'undefined'")).Returns(true); + var config = new Mock(); + config.Setup(x => x.UseServerSideRendering).Returns(true); + + var component = new ReactRouterComponent(environment.Object, config.Object, "Foo", "container") + { + Props = new { hello = "World" } + }; + component.RenderRouterWithContext("/bar"); + + environment.Verify(x => x.Execute(@" + + var context = {}; + + var renderResult = ReactDOMServer.renderToString( + React.createElement(Foo, { path: '/bar', context: context }) + ); + + JSON.stringify({ + renderResult: renderResult, + context: context + }); + ")); + } + + [Fact] + public void RenderHtmlShouldWrapComponentInDiv() + { + var environment = new Mock(); + environment.Setup(x => x.Execute("typeof Foo !== 'undefined'")).Returns(true); + environment.Setup(x => x.Execute(@"ReactDOMServer.renderToString(React.createElement(Foo, {""hello"":""World""}))")) + .Returns("[HTML]"); + var config = new Mock(); + config.Setup(x => x.UseServerSideRendering).Returns(true); + + var component = new ReactComponent(environment.Object, config.Object, "Foo", "container") + { + Props = new { hello = "World" } + }; + var result = component.RenderHtml(); + + Assert.Equal(@"
[HTML]
", result); + } + + [Fact] + public void RenderHtmlShouldNotRenderComponentHtml() + { + var environment = new Mock(); + environment.Setup(x => x.Execute("typeof Foo !== 'undefined'")).Returns(true); + environment.Setup(x => x.Execute(@"React.renderToString(React.createElement(Foo, {""hello"":""World""}))")) + .Returns("[HTML]"); + var config = new Mock(); + + var component = new ReactComponent(environment.Object, config.Object, "Foo", "container") + { + Props = new { hello = "World" } + }; + var result = component.RenderHtml(renderContainerOnly: true); + + Assert.Equal(@"
", result); + environment.Verify(x => x.Execute(It.IsAny()), Times.Never); + } + + [Fact] + public void RenderHtmlShouldNotRenderClientSideAttributes() + { + var environment = new Mock(); + environment.Setup(x => x.Execute("typeof Foo !== 'undefined'")).Returns(true); + var config = new Mock(); + config.Setup(x => x.UseServerSideRendering).Returns(true); + + var component = new ReactComponent(environment.Object, config.Object, "Foo", "container") + { + Props = new { hello = "World" } + }; + component.RenderHtml(renderServerOnly: true); + + environment.Verify(x => x.Execute(@"ReactDOMServer.renderToStaticMarkup(React.createElement(Foo, {""hello"":""World""}))")); + } + + [Fact] + public void RenderHtmlShouldWrapComponentInCustomElement() + { + var config = new Mock(); + config.Setup(x => x.UseServerSideRendering).Returns(true); + var environment = new Mock(); + environment.Setup(x => x.Execute("typeof Foo !== 'undefined'")).Returns(true); + environment.Setup(x => x.Execute(@"ReactDOMServer.renderToString(React.createElement(Foo, {""hello"":""World""}))")) + .Returns("[HTML]"); + + var component = new ReactComponent(environment.Object, config.Object, "Foo", "container") + { + Props = new { hello = "World" }, + ContainerTag = "span" + }; + var result = component.RenderHtml(); + + Assert.Equal(@"[HTML]", result); + } + + [Fact] + public void RenderHtmlShouldAddClassToElement() + { + var config = new Mock(); + config.Setup(x => x.UseServerSideRendering).Returns(true); + var environment = new Mock(); + environment.Setup(x => x.Execute("typeof Foo !== 'undefined'")).Returns(true); + environment.Setup(x => x.Execute(@"ReactDOMServer.renderToString(React.createElement(Foo, {""hello"":""World""}))")) + .Returns("[HTML]"); + + var component = new ReactComponent(environment.Object, config.Object, "Foo", "container") + { + Props = new { hello = "World" }, + ContainerClass="test-class" + }; + var result = component.RenderHtml(); + + Assert.Equal(@"
[HTML]
", result); + } + + [Fact] + public void RenderJavaScriptShouldCallRenderComponent() + { + var environment = new Mock(); + var config = new Mock(); + + var component = new ReactComponent(environment.Object, config.Object, "Foo", "container") + { + Props = new { hello = "World" } + }; + var result = component.RenderJavaScript(); + + Assert.Equal( + @"ReactDOM.render(React.createElement(Foo, {""hello"":""World""}), document.getElementById(""container""))", + result + ); + } + + [Theory] + [InlineData("Foo", true)] + [InlineData("Foo.Bar", true)] + [InlineData("Foo.Bar.Baz", true)] + [InlineData("alert()", false)] + [InlineData("Foo.alert()", false)] + [InlineData("lol what", false)] + public void TestEnsureComponentNameValid(string input, bool expected) + { + var isValid = true; + try + { + ReactComponent.EnsureComponentNameValid(input); + } + catch (ReactInvalidComponentException) + { + isValid = false; + } + Assert.Equal(expected, isValid); + } + + + [Fact] + public void GeneratesContainerIdIfNotProvided() + { + var environment = new Mock(); + var config = new Mock(); + + var component = new ReactComponent(environment.Object, config.Object, "Foo", null); + Assert.StartsWith("react_", component.ContainerId); + } + + } +} From 66cdcbd9d460ab525a4993e0d1ab310358d8b148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunnar=20M=C3=A1r=20=C3=93ttarsson?= Date: Sat, 13 May 2017 20:31:29 +0000 Subject: [PATCH 4/9] Unit tests and final cleanup --- .../Content/Views/web.config.transform | 9 + src/React.Router/ExecutionResult.cs | 31 ++- src/React.Router/HtmlHelperExtensions.cs | 28 ++- src/React.Router/React.Router.csproj | 16 +- .../ReactEnvironmentExtensions.cs | 35 ++- src/React.Router/ReactRouterComponent.cs | 168 ++++++++------ src/React.Router/ReactRouterException.cs | 11 +- src/React.Router/RoutingContext.cs | 35 +-- .../React.Tests/Core/ReactEnvironmentTest.cs | 18 +- .../Owin/EntryAssemblyFileSystemTests.cs | 2 +- .../Router/HtmlHelperExtensionsTest.cs | 212 ++++++++++++++++++ .../Router/ReactEnvironmentExtensionsTest.cs | 36 +++ .../Router/ReactRouterComponentTest.cs | 201 ++--------------- 13 files changed, 503 insertions(+), 299 deletions(-) create mode 100644 src/React.Router/Content/Views/web.config.transform create mode 100644 tests/React.Tests/Router/HtmlHelperExtensionsTest.cs create mode 100644 tests/React.Tests/Router/ReactEnvironmentExtensionsTest.cs diff --git a/src/React.Router/Content/Views/web.config.transform b/src/React.Router/Content/Views/web.config.transform new file mode 100644 index 000000000..c562609d3 --- /dev/null +++ b/src/React.Router/Content/Views/web.config.transform @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/React.Router/ExecutionResult.cs b/src/React.Router/ExecutionResult.cs index ca5f8d08d..ade20da8a 100644 --- a/src/React.Router/ExecutionResult.cs +++ b/src/React.Router/ExecutionResult.cs @@ -1,19 +1,28 @@ -namespace React.Router +/* + * Copyright (c) 2014-Present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +namespace React.Router { /// /// Contains the context object used during execution in addition to /// the string result of rendering the React Router component. /// public class ExecutionResult - { - /// - /// String result of ReactDOMServer render of provided component. - /// - public string renderResult { get; set; } + { + /// + /// String result of ReactDOMServer render of provided component. + /// + public string RenderResult { get; set; } - /// - /// Context object used during JS engine execution. - /// - public RoutingContext context { get; set; } - } + /// + /// Context object used during JS engine execution. + /// + public RoutingContext Context { get; set; } + } } diff --git a/src/React.Router/HtmlHelperExtensions.cs b/src/React.Router/HtmlHelperExtensions.cs index 21b94a2ca..12e38fb0f 100644 --- a/src/React.Router/HtmlHelperExtensions.cs +++ b/src/React.Router/HtmlHelperExtensions.cs @@ -1,4 +1,13 @@ -using System; +/* + * Copyright (c) 2014-Present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +using System; using React.Exceptions; using React.TinyIoC; @@ -55,7 +64,7 @@ private static IReactEnvironment Environment /// MVC Razor /// Name of React Static Router component. Expose component globally to ReactJS.NET /// Props to initialise the component with - /// F.x. from Request.Url.AbsolutePath. Used by React Static Router to determine context and routing. + /// F.x. from Request.Path. Used by React Static Router to determine context and routing. /// Used either by contextHandler or internally to modify the Response status code and redirect. /// Optional custom context handler, can be used instead of providing a Response object /// HTML tag to wrap the component in. Defaults to <div> @@ -83,7 +92,10 @@ public static IHtmlString ReactRouterWithContext( path = path ?? htmlHelper.ViewContext.HttpContext.Request.Path; Response = Response ?? htmlHelper.ViewContext.HttpContext.Response; - var reactComponent = Environment.CreateRouterComponent(componentName, props, containerId, clientOnly); + var reactComponent + = Environment.CreateRouterComponent + (componentName, props, path, containerId, clientOnly); + if (!string.IsNullOrEmpty(htmlTag)) { reactComponent.ContainerTag = htmlTag; @@ -93,23 +105,23 @@ public static IHtmlString ReactRouterWithContext( reactComponent.ContainerClass = containerClass; } - var executionResult = reactComponent.RenderRouterWithContext(path, clientOnly, serverOnly); + var executionResult = reactComponent.RenderRouterWithContext(clientOnly, serverOnly); - if (executionResult.context?.status != null) + if (executionResult.Context?.status != null) { // Use provided contextHandler if (contextHandler != null) { - contextHandler(Response, executionResult.context); + contextHandler(Response, executionResult.Context); } // Handle routing context internally else { - HandleRoutingContext(executionResult.context, Response); + HandleRoutingContext(executionResult.Context, Response); } } - return new HtmlString(executionResult.renderResult); + return new HtmlString(executionResult.RenderResult); } finally { diff --git a/src/React.Router/React.Router.csproj b/src/React.Router/React.Router.csproj index 5f419cef8..58762f6df 100644 --- a/src/React.Router/React.Router.csproj +++ b/src/React.Router/React.Router.csproj @@ -26,12 +26,22 @@ + + true + content\ + + + + + + content\ + true + - @@ -41,6 +51,10 @@ + + + + true diff --git a/src/React.Router/ReactEnvironmentExtensions.cs b/src/React.Router/ReactEnvironmentExtensions.cs index 5b14a2ca2..6c268c039 100644 --- a/src/React.Router/ReactEnvironmentExtensions.cs +++ b/src/React.Router/ReactEnvironmentExtensions.cs @@ -1,10 +1,19 @@ -namespace React.Router +/* + * Copyright (c) 2014-Present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +namespace React.Router { /// /// extension for rendering a React Router Component with context /// public static class ReactEnvironmentExtensions - { + { /// /// Create a React Router Component with context and add it to the list of components to render client side, /// if applicable. @@ -13,20 +22,28 @@ public static class ReactEnvironmentExtensions /// React Environment /// Name of the component /// Props to use + /// F.x. from Request.Path. Used by React Static Router to determine context and routing. /// ID to use for the container HTML tag. Defaults to an auto-generated ID /// True if server-side rendering will be bypassed. Defaults to false. /// public static ReactRouterComponent CreateRouterComponent( - this IReactEnvironment env, - string componentName, - T props, + this IReactEnvironment env, + string componentName, + T props, + string path, string containerId = null, bool clientOnly = false ) - { - var config = React.AssemblyRegistration.Container.Resolve(); + { + var config = AssemblyRegistration.Container.Resolve(); - var component = new ReactRouterComponent(env, config, componentName, containerId) + var component = new ReactRouterComponent( + env, + config, + componentName, + containerId, + path + ) { Props = props, }; @@ -43,5 +60,5 @@ public static ReactRouterComponent CreateRouterComponent( throw new ReactRouterException("Only the default ReactEnvironment is intended to be used with React.Router"); } } - } + } } diff --git a/src/React.Router/ReactRouterComponent.cs b/src/React.Router/ReactRouterComponent.cs index 16e44fa3f..6fbe8f9c3 100644 --- a/src/React.Router/ReactRouterComponent.cs +++ b/src/React.Router/ReactRouterComponent.cs @@ -1,4 +1,13 @@ -using JavaScriptEngineSwitcher.Core; +/* + * Copyright (c) 2014-Present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + + using JavaScriptEngineSwitcher.Core; using Newtonsoft.Json; using React.Exceptions; @@ -10,112 +19,123 @@ namespace React.Router public class ReactRouterComponent : ReactComponent { /// - /// Initializes a new instance of the class. + /// F.x. from Request.Path. Used by React Static Router to determine context and routing. + /// + protected string _path; + + /// + /// Initialises a new instance of the class. /// /// The environment. /// Site-wide configuration. /// Name of the component. /// The ID of the container DIV for this component + /// F.x. from Request.Path. Used by React Static Router to determine context and routing. public ReactRouterComponent( IReactEnvironment environment, IReactSiteConfiguration configuration, string componentName, - string containerId + string containerId, + string path ) : base(environment, configuration, componentName, containerId) { - - } - - /// - /// 's do not use this method. - /// Instead use RenderRouterWithContext. - /// - /// - /// - /// - public override string RenderHtml(bool renderContainerOnly = false, bool renderServerOnly = false) - { - throw new ReactRouterException - (@"React Router Components are rendered with RenderRouterWithContext. - Please use ReactComponent if this functionality is not desired."); + _path = path; } /// /// Render a React StaticRouter Component with context object. /// - /// Current request URL path. F.x. Request.Path /// Only renders component container. Used for client-side only rendering. Does not make sense in this context but included for consistency /// Only renders the common HTML mark up and not any React specific data attributes. Used for server-side only rendering. /// Object containing HTML in string format and the React Router context object - public ExecutionResult RenderRouterWithContext(string path, bool renderContainerOnly = false, bool renderServerOnly = false) + public virtual ExecutionResult RenderRouterWithContext(bool renderContainerOnly = false, bool renderServerOnly = false) { - if (!_configuration.UseServerSideRendering) + if (!EngineSupportsObjectAssign()) { - renderContainerOnly = true; + _environment.Execute(objectAssignPolyfill); } - if (!renderContainerOnly) - { - EnsureComponentExists(); - } + _environment.Execute("var context = {};"); - try - { - ExecutionResult executionResult; + var html = RenderHtml(renderContainerOnly, renderServerOnly); - if (!renderContainerOnly) - { - var componentInitialiser = string.Format("React.createElement({0}, {{ path: '{1}', context: context }})", ComponentName, path ); + var contextString = _environment.Execute("JSON.stringify(context);"); - var reactDomServerMethod = renderServerOnly ? "renderToStaticMarkup" : "renderToString"; - - var reactRenderCommand = string.Format(@" + return new ExecutionResult + { + RenderResult = html, + Context = JsonConvert.DeserializeObject(contextString), + }; + } - var context = {{}}; + /// + /// Gets the JavaScript code to initialise the component + /// + /// JavaScript for component initialisation + protected override string GetComponentInitialiser() + { + return string.Format( + @"React.createElement + ({0}, Object.assign({1}, {{ path: '{2}', context: context }}))", + ComponentName, + _serializedProps, + _path + ); + } - var renderResult = ReactDOMServer.{0}( - React.createElement({1}, {{ path: '{2}', context: context }}) - ); + /// + /// Renders the JavaScript required to initialise this component client-side. This will + /// initialise the React component, which includes attach event handlers to the + /// server-rendered HTML. + /// Uses base Component initialiser. + /// Client side React Router does not need context nor explicit path parameter. + /// + /// JavaScript + public override string RenderJavaScript() + { + return string.Format( + "ReactDOM.render({0}, document.getElementById({1}))", + base.GetComponentInitialiser(), + JsonConvert.SerializeObject(ContainerId, _configuration.JsonSerializerSettings) // SerializeObject accepts null settings + ); + } - JSON.stringify({{ - renderResult: renderResult, - context: context - }}); - ", reactDomServerMethod, ComponentName, path); + /// + /// Ensure js engine supports Object.assign + /// + public virtual bool EngineSupportsObjectAssign() + { + return _environment.Execute( + "typeof Object.assign === 'function'" + ); + } - var strResult = _environment.Execute(reactRenderCommand); - executionResult = JsonConvert.DeserializeObject(strResult); - } - else - { - executionResult = new ExecutionResult(); + /// + /// Polyfill for engines that do not support Object.assign + /// + protected static string objectAssignPolyfill = + @"Object.assign = function(target, varArgs) { // .length of function is 2 + 'use strict'; + if (target == null) { // TypeError if undefined or null + throw new TypeError('Cannot convert undefined or null to object'); } - string attributes = string.Format("id=\"{0}\"", ContainerId); - if (!string.IsNullOrEmpty(ContainerClass)) - { - attributes += string.Format(" class=\"{0}\"", ContainerClass); - } + var to = Object(target); - executionResult.renderResult = - string.Format( - "<{2} {0}>{1}", - attributes, - executionResult.renderResult, - ContainerTag - ); + for (var index = 1; index < arguments.length; index++) { + var nextSource = arguments[index]; - return executionResult; - } - catch (JsRuntimeException ex) - { - throw new ReactServerRenderingException(string.Format( - "Error while rendering \"{0}\" to \"{2}\": {1}", - ComponentName, - ex.Message, - ContainerId - )); - } - } + if (nextSource != null) { // Skip over if undefined or null + for (var nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }; + "; } } diff --git a/src/React.Router/ReactRouterException.cs b/src/React.Router/ReactRouterException.cs index 5dde76974..419397be4 100644 --- a/src/React.Router/ReactRouterException.cs +++ b/src/React.Router/ReactRouterException.cs @@ -1,4 +1,13 @@ -using System; +/* + * Copyright (c) 2014-Present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +using System; using System.Runtime.Serialization; namespace React.Router diff --git a/src/React.Router/RoutingContext.cs b/src/React.Router/RoutingContext.cs index efb313ff6..49e00ca9e 100644 --- a/src/React.Router/RoutingContext.cs +++ b/src/React.Router/RoutingContext.cs @@ -1,20 +1,29 @@ -namespace React.Router +/* + * Copyright (c) 2014-Present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +namespace React.Router { /// /// Context object used during render of React Router component /// public class RoutingContext - { - /// - /// HTTP Status Code. - /// If present signifies that the given status code should be returned by server. - /// - public int? status { get; set; } + { + /// + /// HTTP Status Code. + /// If present signifies that the given status code should be returned by server. + /// + public int? status { get; set; } - /// - /// URL to redirect to. - /// If included this signals that React Router determined a redirect should happen. - /// - public string url { get; set; } - } + /// + /// URL to redirect to. + /// If included this signals that React Router determined a redirect should happen. + /// + public string url { get; set; } + } } diff --git a/tests/React.Tests/Core/ReactEnvironmentTest.cs b/tests/React.Tests/Core/ReactEnvironmentTest.cs index a8b478491..8e437a0b3 100644 --- a/tests/React.Tests/Core/ReactEnvironmentTest.cs +++ b/tests/React.Tests/Core/ReactEnvironmentTest.cs @@ -113,7 +113,7 @@ public void ReturnsEngineToPool() var mocks = new Mocks(); var environment = mocks.CreateReactEnvironment(); mocks.Config.Setup(x => x.ReuseJavaScriptEngines).Returns(true); - + environment.CreateComponent("ComponentName", new { }); mocks.EngineFactory.Verify(x => x.GetEngine(), Times.Once); environment.ReturnEngineToPool(); @@ -122,7 +122,21 @@ public void ReturnsEngineToPool() mocks.EngineFactory.Verify(x => x.GetEngine(), Times.AtLeast(2)); } - private class Mocks + [Fact] + public void CreatesIReactComponent() + { + var mocks = new Mocks(); + var environment = mocks.CreateReactEnvironment(); + + var component = new Mock(); + + environment.CreateComponent(component.Object); + + // A single nameless component was successfully added! + Assert.Equal(";\r\n", environment.GetInitJavaScript()); + } + + public class Mocks { public Mock Engine { get; private set; } public Mock EngineFactory { get; private set; } diff --git a/tests/React.Tests/Owin/EntryAssemblyFileSystemTests.cs b/tests/React.Tests/Owin/EntryAssemblyFileSystemTests.cs index d14d0f9e1..4f456e9a3 100644 --- a/tests/React.Tests/Owin/EntryAssemblyFileSystemTests.cs +++ b/tests/React.Tests/Owin/EntryAssemblyFileSystemTests.cs @@ -14,7 +14,7 @@ namespace React.Tests.Owin { public class EntryAssemblyFileSystemTests { - [Theory] + [Theory] [InlineData("C:\\", "~/", "C:\\")] [InlineData("C:\\", "~/foo/bar.js", "C:\\foo\\bar.js")] public void MapPath(string rootPath, string relativePath, string expected) diff --git a/tests/React.Tests/Router/HtmlHelperExtensionsTest.cs b/tests/React.Tests/Router/HtmlHelperExtensionsTest.cs new file mode 100644 index 000000000..2e4816e95 --- /dev/null +++ b/tests/React.Tests/Router/HtmlHelperExtensionsTest.cs @@ -0,0 +1,212 @@ +/* + * Copyright (c) 2014-Present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +using Moq; +using Xunit; +using React.Exceptions; +using React.Router; +using React.Tests.Core; +using System.Web; + +namespace React.Tests.Router +{ + public class HtmlHelperExtensionsTest + { + /// + /// Creates a mock and registers it with the IoC container + /// This is only required because can not be + /// injected :( + /// + private ReactEnvironmentTest.Mocks ConfigureMockReactEnvironment() + { + var mocks = new ReactEnvironmentTest.Mocks(); + + var environment = mocks.CreateReactEnvironment(); + AssemblyRegistration.Container.Register(environment); + return mocks; + } + + private Mock ConfigureMockEnvironment() + { + var environment = new Mock(); + AssemblyRegistration.Container.Register(environment.Object); + return environment; + } + + private Mock ConfigureMockConfiguration() + { + var config = new Mock(); + AssemblyRegistration.Container.Register(config.Object); + return config; + } + + //[Fact] + //public void EngineIsReturnedToPoolAfterRender() + //{ + // var component = new Mock(); + // component.Setup(x => x.RenderHtml(true, true)).Returns("HTML"); + // var environment = ConfigureMockReactEnvironment(); + // environment.Setup(x => x.CreateComponent( + // "ComponentName", + // new { }, + // null, + // true + // )).Returns(component.Object); + + // environment.Verify(x => x.ReturnEngineToPool(), Times.Never); + // var result = HtmlHelperExtensions.ReactRouterWithContext( + // htmlHelper: null, + // componentName: "ComponentName", + // props: new { }, + // path: "/", + // htmlTag: "span", + // clientOnly: true, + // serverOnly: true + // ); + // component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == true)), Times.Once); + // environment.Verify(x => x.ReturnEngineToPool(), Times.Once); + //} + + //[Fact] + //public void ReactWithClientOnlyTrueShouldCallRenderHtmlWithTrue() + //{ + // var component = new Mock(); + // component.Setup(x => x.RenderHtml(true, true)).Returns("HTML"); + // var environment = ConfigureMockEnvironment(); + // environment.Setup(x => x.CreateComponent( + // "ComponentName", + // new { }, + // null, + // true + // )).Returns(component.Object); + + // var result = HtmlHelperExtensions.ReactRouterWithContext( + // htmlHelper: null, + // componentName: "ComponentName", + // props: new { }, + // htmlTag: "span", + // clientOnly: true, + // serverOnly: true + // ); + // component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == true)), Times.Once); + //} + + //[Fact] + //public void ReactWithServerOnlyTrueShouldCallRenderHtmlWithTrue() + //{ + // var component = new Mock(); + // component.Setup(x => x.RenderHtml(true, true)).Returns("HTML"); + // var environment = ConfigureMockEnvironment(); + // environment.Setup(x => x.CreateComponent( + // "ComponentName", + // new { }, + // null, + // true + // )).Returns(component.Object); + + // var result = HtmlHelperExtensions.React( + // htmlHelper: null, + // componentName: "ComponentName", + // props: new { }, + // htmlTag: "span", + // clientOnly: true, + // serverOnly: true + // ); + // component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == true)), Times.Once); + //} + + [Fact] + public void ShouldModifyStatusCode() + { + var mocks = ConfigureMockReactEnvironment(); + ConfigureMockConfiguration(); + + mocks.Engine.Setup(x => x.Evaluate("JSON.stringify(context);")) + .Returns("{ status: 200 }"); + + var httpResponse = new Mock(); + + HtmlHelperExtensions.ReactRouterWithContext( + htmlHelper: null, + componentName: "ComponentName", + props: new { }, + path: "/", + Response: httpResponse.Object + ); + httpResponse.VerifySet(x => x.StatusCode = 200); + } + + [Fact] + public void ShouldRunCustomContextHandler() + { + + var mocks = ConfigureMockReactEnvironment(); + ConfigureMockConfiguration(); + + mocks.Engine.Setup(x => x.Evaluate("JSON.stringify(context);")) + .Returns("{ status: 200 }"); + + var httpResponse = new Mock(); + + HtmlHelperExtensions.ReactRouterWithContext( + htmlHelper: null, + componentName: "ComponentName", + props: new { }, + path: "/", + Response: httpResponse.Object, + contextHandler: (response, context) => response.StatusCode = context.status.Value + ); + httpResponse.VerifySet(x => x.StatusCode = 200); + } + + [Fact] + public void ShouldRedirectPermanent() + { + var mocks = ConfigureMockReactEnvironment(); + ConfigureMockConfiguration(); + + mocks.Engine.Setup(x => x.Evaluate("JSON.stringify(context);")) + .Returns(@"{ status: 301, url: ""/foo"" }"); + + var httpResponse = new Mock(); + + HtmlHelperExtensions.ReactRouterWithContext( + htmlHelper: null, + componentName: "ComponentName", + props: new { }, + path: "/", + Response: httpResponse.Object + ); + httpResponse.Verify(x => x.Redirect(It.IsAny(), true)); + } + + [Fact] + public void ShouldFailRedirectWithNoUrl() + { + var mocks = ConfigureMockReactEnvironment(); + ConfigureMockConfiguration(); + + mocks.Engine.Setup(x => x.Evaluate("JSON.stringify(context);")) + .Returns("{ status: 301 }"); + + var httpResponse = new Mock(); + + Assert.Throws(() => + + HtmlHelperExtensions.ReactRouterWithContext( + htmlHelper: null, + componentName: "ComponentName", + props: new { }, + path: "/", + Response: httpResponse.Object + ) + ); + } + } +} diff --git a/tests/React.Tests/Router/ReactEnvironmentExtensionsTest.cs b/tests/React.Tests/Router/ReactEnvironmentExtensionsTest.cs new file mode 100644 index 000000000..6689385b0 --- /dev/null +++ b/tests/React.Tests/Router/ReactEnvironmentExtensionsTest.cs @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2014-Present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +using Moq; +using Xunit; +using React.Exceptions; +using React.Router; +using React.Tests.Core; + +namespace React.Tests.Router +{ + public class ReactEnvironmentExtensionsTest + { + [Fact] + public void EnvironmentShouldGetCalledClientOnly() + { + var environment = new Mock(); + var component = ReactEnvironmentExtensions.CreateRouterComponent( + environment.Object, + "ComponentName", + new { }, + "/", + null, + true + ); + + environment.Verify(x => x.CreateComponent("ComponentName", new { }, null, true)); + } + } +} diff --git a/tests/React.Tests/Router/ReactRouterComponentTest.cs b/tests/React.Tests/Router/ReactRouterComponentTest.cs index 9bf26023c..f44577725 100644 --- a/tests/React.Tests/Router/ReactRouterComponentTest.cs +++ b/tests/React.Tests/Router/ReactRouterComponentTest.cs @@ -11,163 +11,19 @@ using Xunit; using React.Exceptions; using React.Router; +using React.Tests.Core; namespace React.Tests.Router { public class ReactRouterComponentTest { [Fact] - public void RenderHtmlShouldThrowExceptionIfComponentDoesNotExist() + public void RenderJavaScriptShouldNotIncludeContextOrPath() { var environment = new Mock(); - environment.Setup(x => x.Execute("typeof Foo !== 'undefined'")).Returns(false); var config = new Mock(); - config.Setup(x => x.UseServerSideRendering).Returns(true); - var component = new ReactComponent(environment.Object, config.Object, "Foo", "container"); - Assert.Throws(() => - { - component.RenderHtml(); - }); - } - - [Fact] - public void blabla() - { - new Mock().Verify(x => x.ToString()); - } - - [Fact] - public void RenderHtmlShouldCallRenderComponent() - { - var environment = new Mock(); - environment.Setup(x => x.Execute("typeof Foo !== 'undefined'")).Returns(true); - var config = new Mock(); - config.Setup(x => x.UseServerSideRendering).Returns(true); - - var component = new ReactRouterComponent(environment.Object, config.Object, "Foo", "container") - { - Props = new { hello = "World" } - }; - component.RenderRouterWithContext("/bar"); - - environment.Verify(x => x.Execute(@" - - var context = {}; - - var renderResult = ReactDOMServer.renderToString( - React.createElement(Foo, { path: '/bar', context: context }) - ); - - JSON.stringify({ - renderResult: renderResult, - context: context - }); - ")); - } - - [Fact] - public void RenderHtmlShouldWrapComponentInDiv() - { - var environment = new Mock(); - environment.Setup(x => x.Execute("typeof Foo !== 'undefined'")).Returns(true); - environment.Setup(x => x.Execute(@"ReactDOMServer.renderToString(React.createElement(Foo, {""hello"":""World""}))")) - .Returns("[HTML]"); - var config = new Mock(); - config.Setup(x => x.UseServerSideRendering).Returns(true); - - var component = new ReactComponent(environment.Object, config.Object, "Foo", "container") - { - Props = new { hello = "World" } - }; - var result = component.RenderHtml(); - - Assert.Equal(@"
[HTML]
", result); - } - - [Fact] - public void RenderHtmlShouldNotRenderComponentHtml() - { - var environment = new Mock(); - environment.Setup(x => x.Execute("typeof Foo !== 'undefined'")).Returns(true); - environment.Setup(x => x.Execute(@"React.renderToString(React.createElement(Foo, {""hello"":""World""}))")) - .Returns("[HTML]"); - var config = new Mock(); - - var component = new ReactComponent(environment.Object, config.Object, "Foo", "container") - { - Props = new { hello = "World" } - }; - var result = component.RenderHtml(renderContainerOnly: true); - - Assert.Equal(@"
", result); - environment.Verify(x => x.Execute(It.IsAny()), Times.Never); - } - - [Fact] - public void RenderHtmlShouldNotRenderClientSideAttributes() - { - var environment = new Mock(); - environment.Setup(x => x.Execute("typeof Foo !== 'undefined'")).Returns(true); - var config = new Mock(); - config.Setup(x => x.UseServerSideRendering).Returns(true); - - var component = new ReactComponent(environment.Object, config.Object, "Foo", "container") - { - Props = new { hello = "World" } - }; - component.RenderHtml(renderServerOnly: true); - - environment.Verify(x => x.Execute(@"ReactDOMServer.renderToStaticMarkup(React.createElement(Foo, {""hello"":""World""}))")); - } - - [Fact] - public void RenderHtmlShouldWrapComponentInCustomElement() - { - var config = new Mock(); - config.Setup(x => x.UseServerSideRendering).Returns(true); - var environment = new Mock(); - environment.Setup(x => x.Execute("typeof Foo !== 'undefined'")).Returns(true); - environment.Setup(x => x.Execute(@"ReactDOMServer.renderToString(React.createElement(Foo, {""hello"":""World""}))")) - .Returns("[HTML]"); - - var component = new ReactComponent(environment.Object, config.Object, "Foo", "container") - { - Props = new { hello = "World" }, - ContainerTag = "span" - }; - var result = component.RenderHtml(); - - Assert.Equal(@"[HTML]", result); - } - - [Fact] - public void RenderHtmlShouldAddClassToElement() - { - var config = new Mock(); - config.Setup(x => x.UseServerSideRendering).Returns(true); - var environment = new Mock(); - environment.Setup(x => x.Execute("typeof Foo !== 'undefined'")).Returns(true); - environment.Setup(x => x.Execute(@"ReactDOMServer.renderToString(React.createElement(Foo, {""hello"":""World""}))")) - .Returns("[HTML]"); - - var component = new ReactComponent(environment.Object, config.Object, "Foo", "container") - { - Props = new { hello = "World" }, - ContainerClass="test-class" - }; - var result = component.RenderHtml(); - - Assert.Equal(@"
[HTML]
", result); - } - - [Fact] - public void RenderJavaScriptShouldCallRenderComponent() - { - var environment = new Mock(); - var config = new Mock(); - - var component = new ReactComponent(environment.Object, config.Object, "Foo", "container") + var component = new ReactRouterComponent(environment.Object, config.Object, "Foo", "container", "/bar") { Props = new { hello = "World" } }; @@ -179,37 +35,24 @@ public void RenderJavaScriptShouldCallRenderComponent() ); } - [Theory] - [InlineData("Foo", true)] - [InlineData("Foo.Bar", true)] - [InlineData("Foo.Bar.Baz", true)] - [InlineData("alert()", false)] - [InlineData("Foo.alert()", false)] - [InlineData("lol what", false)] - public void TestEnsureComponentNameValid(string input, bool expected) - { - var isValid = true; - try - { - ReactComponent.EnsureComponentNameValid(input); - } - catch (ReactInvalidComponentException) - { - isValid = false; - } - Assert.Equal(expected, isValid); - } - - - [Fact] - public void GeneratesContainerIdIfNotProvided() - { - var environment = new Mock(); - var config = new Mock(); - - var component = new ReactComponent(environment.Object, config.Object, "Foo", null); - Assert.StartsWith("react_", component.ContainerId); - } - + //[Fact] + //public void ShouldUsePolifyfillSuccessfully() + //{ + // var mocks = new ReactEnvironmentTest.Mocks(); + // var environment = mocks.CreateReactEnvironment(); + // //environment.Setup(x => x.Execute("typeof Object.assign === 'function'")).Returns(false); + // //environment.Setup(x => x.Execute("JSON.stringify(context);")).Returns("{}"); + + // var config = new Mock(); + // config.Setup(x => x.UseServerSideRendering).Returns(false); + + // var component = new ReactRouterComponent(environment, config.Object, "Foo", "container", "/bar") + // { + // Props = new { hello = "World" } + // }; + // var html = component.RenderRouterWithContext(true); + // var result = environment.Execute(@"Object.assign(""a"", { sup=""yo""})"); + // Assert.Equal(@"{ [string: ""a""], sup: ""yo""", result); + //} } } From 57f07ed7a0db7409be91d28341565c1993ae7ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunnar=20M=C3=A1r=20=C3=93ttarsson?= Date: Sat, 13 May 2017 20:42:32 +0000 Subject: [PATCH 5/9] Fix htmlhelper test and comment out environmentextensionsTest --- .../Router/HtmlHelperExtensionsTest.cs | 2 +- .../Router/ReactEnvironmentExtensionsTest.cs | 38 +++++++++++++------ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/tests/React.Tests/Router/HtmlHelperExtensionsTest.cs b/tests/React.Tests/Router/HtmlHelperExtensionsTest.cs index 2e4816e95..e5d8a2921 100644 --- a/tests/React.Tests/Router/HtmlHelperExtensionsTest.cs +++ b/tests/React.Tests/Router/HtmlHelperExtensionsTest.cs @@ -183,7 +183,7 @@ public void ShouldRedirectPermanent() path: "/", Response: httpResponse.Object ); - httpResponse.Verify(x => x.Redirect(It.IsAny(), true)); + httpResponse.Verify(x => x.RedirectPermanent(It.IsAny())); } [Fact] diff --git a/tests/React.Tests/Router/ReactEnvironmentExtensionsTest.cs b/tests/React.Tests/Router/ReactEnvironmentExtensionsTest.cs index 6689385b0..10a974f55 100644 --- a/tests/React.Tests/Router/ReactEnvironmentExtensionsTest.cs +++ b/tests/React.Tests/Router/ReactEnvironmentExtensionsTest.cs @@ -17,20 +17,34 @@ namespace React.Tests.Router { public class ReactEnvironmentExtensionsTest { - [Fact] - public void EnvironmentShouldGetCalledClientOnly() + /// + /// Creates a mock and registers it with the IoC container + /// This is only required because can not be + /// injected :( + /// + private ReactEnvironment ConfigureMockReactEnvironment() { - var environment = new Mock(); - var component = ReactEnvironmentExtensions.CreateRouterComponent( - environment.Object, - "ComponentName", - new { }, - "/", - null, - true - ); + var mocks = new ReactEnvironmentTest.Mocks(); - environment.Verify(x => x.CreateComponent("ComponentName", new { }, null, true)); + var environment = mocks.CreateReactEnvironment(); + AssemblyRegistration.Container.Register(environment); + return environment; } + + // [Fact] + //public void EnvironmentShouldGetCalledClientOnly() + //{ + // var environment = ConfigureMockReactEnvironment(); + // var component = ReactEnvironmentExtensions.CreateRouterComponent( + // environment.Object, + // "ComponentName", + // new { }, + // "/", + // null, + // true + // ); + + // environment.Verify(x => x.CreateComponent("ComponentName", new { }, null, true)); + //} } } From c04716e20e41c9f350acaa295f0b2f4fb217a144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunnar=20M=C3=A1r=20=C3=93ttarsson?= Date: Sat, 13 May 2017 22:31:37 +0000 Subject: [PATCH 6/9] Fix content include and only include aspnetcore files in netstandard build --- src/React.Router/React.Router.csproj | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/React.Router/React.Router.csproj b/src/React.Router/React.Router.csproj index 58762f6df..2311c3e9a 100644 --- a/src/React.Router/React.Router.csproj +++ b/src/React.Router/React.Router.csproj @@ -32,14 +32,7 @@ - - - content\ - true - - - - + From e6f570d80f6e50b0ff48eed4767e86218c2223a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunnar=20M=C3=A1r=20=C3=93ttarsson?= Date: Sun, 14 May 2017 01:22:47 +0000 Subject: [PATCH 7/9] new createComponent function added to environment interface --- src/React.Core/IReactEnvironment.cs | 8 +++++ .../ReactEnvironmentExtensions.cs | 13 ++------ .../Router/ReactEnvironmentExtensionsTest.cs | 30 +++++++++---------- 3 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/React.Core/IReactEnvironment.cs b/src/React.Core/IReactEnvironment.cs index f73eb6e42..39fccd721 100644 --- a/src/React.Core/IReactEnvironment.cs +++ b/src/React.Core/IReactEnvironment.cs @@ -84,6 +84,14 @@ public interface IReactEnvironment /// The component IReactComponent CreateComponent(string componentName, T props, string containerId = null, bool clientOnly = false); + /// + /// Adds the provided to the list of components to render client side. + /// + /// Component to add to client side render list + /// True if server-side rendering will be bypassed. Defaults to false. + /// The component + IReactComponent CreateComponent(IReactComponent component, bool clientOnly = false); + /// /// Renders the JavaScript required to initialise all components client-side. This will /// attach event handlers to the server-rendered HTML. diff --git a/src/React.Router/ReactEnvironmentExtensions.cs b/src/React.Router/ReactEnvironmentExtensions.cs index 6c268c039..809580d8e 100644 --- a/src/React.Router/ReactEnvironmentExtensions.cs +++ b/src/React.Router/ReactEnvironmentExtensions.cs @@ -48,17 +48,8 @@ public static ReactRouterComponent CreateRouterComponent( Props = props, }; - var reactEnvironment = env as ReactEnvironment; - - if (reactEnvironment != null) - { - reactEnvironment.CreateComponent(component, clientOnly); - return component; - } - else - { - throw new ReactRouterException("Only the default ReactEnvironment is intended to be used with React.Router"); - } + env.CreateComponent(component, clientOnly); + return component; } } } diff --git a/tests/React.Tests/Router/ReactEnvironmentExtensionsTest.cs b/tests/React.Tests/Router/ReactEnvironmentExtensionsTest.cs index 10a974f55..9df351251 100644 --- a/tests/React.Tests/Router/ReactEnvironmentExtensionsTest.cs +++ b/tests/React.Tests/Router/ReactEnvironmentExtensionsTest.cs @@ -31,20 +31,20 @@ private ReactEnvironment ConfigureMockReactEnvironment() return environment; } - // [Fact] - //public void EnvironmentShouldGetCalledClientOnly() - //{ - // var environment = ConfigureMockReactEnvironment(); - // var component = ReactEnvironmentExtensions.CreateRouterComponent( - // environment.Object, - // "ComponentName", - // new { }, - // "/", - // null, - // true - // ); + //[Fact] + //public void EnvironmentShouldGetCalledClientOnly() + //{ + // var environment = ConfigureMockReactEnvironment(); + // var component = ReactEnvironmentExtensions.CreateRouterComponent( + // environment.Object, + // "ComponentName", + // new { }, + // "/", + // null, + // true + // ); - // environment.Verify(x => x.CreateComponent("ComponentName", new { }, null, true)); - //} - } + // environment.Verify(x => x.CreateComponent("ComponentName", new { }, null, true)); + //} + } } From c2514c4b7c150ab2ce3133a6c5eed533db0f7c2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunnar=20M=C3=A1r=20=C3=93ttarsson?= Date: Sun, 9 Jul 2017 21:02:42 +0000 Subject: [PATCH 8/9] Various changes after feedback from Daniel. --- src/.editorconfig => .editorconfig | 0 build.proj | 10 +- src/React.AspNet/HtmlHelperExtensions.cs | 17 +- src/React.Core/ReactComponent.cs | 2 +- src/React.Core/ReactEnvironment.cs | 29 +++ src/React.Core/Resources/shims.js | 28 +++ src/React.Router/HtmlHelperExtensions.cs | 70 +------ src/React.Router/React.Router.csproj | 101 +++++---- src/React.Router/ReactRouterComponent.cs | 45 +--- src/React.Router/SetServerResponse.cs | 70 +++++++ src/React.sln | 3 +- .../React.Tests/Core/ReactEnvironmentTest.cs | 13 +- .../Router/HtmlHelperExtensionsTest.cs | 193 ++++++++++-------- .../Router/ReactEnvironmentExtensionsTest.cs | 44 ++-- .../Router/ReactRouterComponentTest.cs | 20 -- 15 files changed, 336 insertions(+), 309 deletions(-) rename src/.editorconfig => .editorconfig (100%) create mode 100644 src/React.Router/SetServerResponse.cs diff --git a/src/.editorconfig b/.editorconfig similarity index 100% rename from src/.editorconfig rename to .editorconfig diff --git a/build.proj b/build.proj index 165c28b9d..f5670fd70 100644 --- a/build.proj +++ b/build.proj @@ -10,8 +10,8 @@ of patent rights can be found in the PATENTS file in the same directory. 3 - 1 - 0 + 0 + 1 0 http://reactjs.net/packages/ $(MSBuildProjectDirectory)\tools\MSBuildTasks @@ -26,9 +26,9 @@ of patent rights can be found in the PATENTS file in the same directory. - - - + + + diff --git a/src/React.AspNet/HtmlHelperExtensions.cs b/src/React.AspNet/HtmlHelperExtensions.cs index f1cdd0201..cb6b168c9 100644 --- a/src/React.AspNet/HtmlHelperExtensions.cs +++ b/src/React.AspNet/HtmlHelperExtensions.cs @@ -39,22 +39,7 @@ private static IReactEnvironment Environment { get { - try - { - return ReactEnvironment.Current; - } - catch (TinyIoCResolutionException ex) - { - throw new ReactNotInitialisedException( -#if LEGACYASPNET - "ReactJS.NET has not been initialised correctly.", -#else - "ReactJS.NET has not been initialised correctly. Please ensure you have " + - "called services.AddReact() and app.UseReact() in your Startup.cs file.", -#endif - ex - ); - } + return ReactEnvironment.GetCurrentOrThrow; } } diff --git a/src/React.Core/ReactComponent.cs b/src/React.Core/ReactComponent.cs index 971e4ce4b..140a1bb5b 100644 --- a/src/React.Core/ReactComponent.cs +++ b/src/React.Core/ReactComponent.cs @@ -142,7 +142,7 @@ public virtual string RenderHtml(bool renderContainerOnly = false, bool renderSe attributes, html, ContainerTag - ); + ); } catch (JsRuntimeException ex) { diff --git a/src/React.Core/ReactEnvironment.cs b/src/React.Core/ReactEnvironment.cs index 70d3b9716..36a4e546b 100644 --- a/src/React.Core/ReactEnvironment.cs +++ b/src/React.Core/ReactEnvironment.cs @@ -17,6 +17,7 @@ using JavaScriptEngineSwitcher.Core.Helpers; using Newtonsoft.Json; using React.Exceptions; +using React.TinyIoC; namespace React { @@ -84,6 +85,34 @@ public static IReactEnvironment Current get { return AssemblyRegistration.Container.Resolve(); } } + /// + /// Gets the for the current request. If no environment + /// has been created for the current request, creates a new one. + /// Also provides more specific error information in the event that ReactJS.NET is misconfigured. + /// + public static IReactEnvironment GetCurrentOrThrow + { + get + { + try + { + return Current; + } + catch (TinyIoCResolutionException ex) + { + throw new ReactNotInitialisedException( +#if NET451 + "ReactJS.NET has not been initialised correctly.", +#else + "ReactJS.NET has not been initialised correctly. Please ensure you have " + + "called services.AddReact() and app.UseReact() in your Startup.cs file.", +#endif + ex + ); + } + } + } + /// /// Initializes a new instance of the class. /// diff --git a/src/React.Core/Resources/shims.js b/src/React.Core/Resources/shims.js index d73353df3..ae90b3156 100644 --- a/src/React.Core/Resources/shims.js +++ b/src/React.Core/Resources/shims.js @@ -65,3 +65,31 @@ function ReactNET_initReact() { // :'( return false; } + +/** + * Polyfill for engines that do not support Object.assign + */ +if (typeof Object.assign !== 'function') { + Object.assign = function (target, varArgs) { // .length of function is 2 + 'use strict'; + if (target == null) { // TypeError if undefined or null + throw new TypeError('Cannot convert undefined or null to object'); + } + + var to = Object(target); + + for (var index = 1; index < arguments.length; index++) { + var nextSource = arguments[index]; + + if (nextSource != null) { // Skip over if undefined or null + for (var nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }; +} \ No newline at end of file diff --git a/src/React.Router/HtmlHelperExtensions.cs b/src/React.Router/HtmlHelperExtensions.cs index 12e38fb0f..cabd14c6f 100644 --- a/src/React.Router/HtmlHelperExtensions.cs +++ b/src/React.Router/HtmlHelperExtensions.cs @@ -37,35 +37,18 @@ private static IReactEnvironment Environment { get { - try - { - return ReactEnvironment.Current; - } - catch (TinyIoCResolutionException ex) - { - throw new ReactNotInitialisedException( -#if NET451 - "ReactJS.NET has not been initialised correctly.", -#else - "ReactJS.NET has not been initialised correctly. Please ensure you have " + - "called services.AddReact() and app.UseReact() in your Startup.cs file.", -#endif - ex - ); - } + return ReactEnvironment.GetCurrentOrThrow; } } /// /// Render a React StaticRouter Component with context object. /// Can optionally be provided with a custom context handler to handle the various status codes. - /// /// /// MVC Razor /// Name of React Static Router component. Expose component globally to ReactJS.NET /// Props to initialise the component with /// F.x. from Request.Path. Used by React Static Router to determine context and routing. - /// Used either by contextHandler or internally to modify the Response status code and redirect. /// Optional custom context handler, can be used instead of providing a Response object /// HTML tag to wrap the component in. Defaults to <div> /// ID to use for the container HTML tag. Defaults to an auto-generated ID @@ -78,7 +61,6 @@ public static IHtmlString ReactRouterWithContext( string componentName, T props, string path = null, - HttpResponse Response = null, string htmlTag = null, string containerId = null, bool clientOnly = false, @@ -89,12 +71,17 @@ public static IHtmlString ReactRouterWithContext( { try { + var response = htmlHelper.ViewContext.HttpContext.Response; path = path ?? htmlHelper.ViewContext.HttpContext.Request.Path; - Response = Response ?? htmlHelper.ViewContext.HttpContext.Response; var reactComponent - = Environment.CreateRouterComponent - (componentName, props, path, containerId, clientOnly); + = Environment.CreateRouterComponent( + componentName, + props, + path, + containerId, + clientOnly + ); if (!string.IsNullOrEmpty(htmlTag)) { @@ -112,12 +99,12 @@ var reactComponent // Use provided contextHandler if (contextHandler != null) { - contextHandler(Response, executionResult.Context); + contextHandler(response, executionResult.Context); } // Handle routing context internally else { - HandleRoutingContext(executionResult.Context, Response); + SetServerResponse.ModifyResponse(executionResult.Context, response); } } @@ -128,40 +115,5 @@ var reactComponent Environment.ReturnEngineToPool(); } } - - private static void HandleRoutingContext(RoutingContext context, HttpResponse Response) - { - var statusCode = context.status.Value; - - // 300-399 - if (statusCode >= 300 && statusCode < 400) - { - if (!string.IsNullOrEmpty(context.url)) - { - if (statusCode == 301) - { - -#if NET451 - - Response.RedirectPermanent(context.url); -#else - Response.Redirect(context.url, true); -#endif - } - else // 302 and all others - { - Response.Redirect(context.url); - } - } - else - { - throw new ReactRouterException("Router requested redirect but no url provided."); - } - } - else - { - Response.StatusCode = statusCode; - } - } } } diff --git a/src/React.Router/React.Router.csproj b/src/React.Router/React.Router.csproj index 2311c3e9a..c03204a80 100644 --- a/src/React.Router/React.Router.csproj +++ b/src/React.Router/React.Router.csproj @@ -1,55 +1,54 @@  - - React Router support for ReactJS.NET. - Copyright 2014-Present Facebook, Inc - ReactJS.NET Router - Daniel Lo Nigro, Gunnar Már Óttarsson - net451;netstandard1.6 - true - React.Router - ../key.snk - true - true - React.Router - asp.net;mvc;asp;javascript;js;react;facebook;reactjs;babel - http://reactjs.net/img/logo_64.png - http://reactjs.net/ - https://github.com/reactjs/React.NET#licence - false - - - - TRACE;DEBUG;ASPNETCORE;NET451 - - - - - - - true - content\ - - - - - - - - - - - - - - - - - - - - - true - + + React Router support for ReactJS.NET. + Copyright 2014-Present Facebook, Inc + ReactJS.NET Router + Daniel Lo Nigro, Gunnar Már Óttarsson + net451;netstandard1.6 + true + React.Router + ../key.snk + true + true + React.Router + asp.net;mvc;asp;javascript;js;react;facebook;reactjs;babel;router;react router + http://reactjs.net/img/logo_64.png + http://reactjs.net/ + https://github.com/reactjs/React.NET#licence + false + + + + TRACE;DEBUG;ASPNETCORE;NET451 + + + + + + + true + content\ + + + + + + + + + + + + + + + + + + + + true + diff --git a/src/React.Router/ReactRouterComponent.cs b/src/React.Router/ReactRouterComponent.cs index 6fbe8f9c3..c2d553027 100644 --- a/src/React.Router/ReactRouterComponent.cs +++ b/src/React.Router/ReactRouterComponent.cs @@ -7,7 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ - using JavaScriptEngineSwitcher.Core; +using JavaScriptEngineSwitcher.Core; using Newtonsoft.Json; using React.Exceptions; @@ -50,11 +50,6 @@ string path /// Object containing HTML in string format and the React Router context object public virtual ExecutionResult RenderRouterWithContext(bool renderContainerOnly = false, bool renderServerOnly = false) { - if (!EngineSupportsObjectAssign()) - { - _environment.Execute(objectAssignPolyfill); - } - _environment.Execute("var context = {};"); var html = RenderHtml(renderContainerOnly, renderServerOnly); @@ -99,43 +94,5 @@ public override string RenderJavaScript() JsonConvert.SerializeObject(ContainerId, _configuration.JsonSerializerSettings) // SerializeObject accepts null settings ); } - - /// - /// Ensure js engine supports Object.assign - /// - public virtual bool EngineSupportsObjectAssign() - { - return _environment.Execute( - "typeof Object.assign === 'function'" - ); - } - - /// - /// Polyfill for engines that do not support Object.assign - /// - protected static string objectAssignPolyfill = - @"Object.assign = function(target, varArgs) { // .length of function is 2 - 'use strict'; - if (target == null) { // TypeError if undefined or null - throw new TypeError('Cannot convert undefined or null to object'); - } - - var to = Object(target); - - for (var index = 1; index < arguments.length; index++) { - var nextSource = arguments[index]; - - if (nextSource != null) { // Skip over if undefined or null - for (var nextKey in nextSource) { - // Avoid bugs when hasOwnProperty is shadowed - if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { - to[nextKey] = nextSource[nextKey]; - } - } - } - } - return to; - }; - "; } } diff --git a/src/React.Router/SetServerResponse.cs b/src/React.Router/SetServerResponse.cs new file mode 100644 index 000000000..3f225a3bc --- /dev/null +++ b/src/React.Router/SetServerResponse.cs @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2014-Present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#if NET451 +using HttpResponse = System.Web.HttpResponseBase; +#else +using HttpResponse = Microsoft.AspNetCore.Http.HttpResponse; +#endif + +namespace React.Router +{ + /// + /// Helper class that takes the values in the + /// to modify the servers response. + /// F.x. return an http status code of 404 not found + /// or redirect the client to a new URL. + /// + public static class SetServerResponse + { + /// + /// Uses the values in the to modify + /// the servers response. + /// F.x. return an http status code of 404 not found + /// or redirect the client to a new URL. + /// + /// + /// The routing context returned by React Router. + /// It contains new values for the server response. + /// + /// The response object to use. + public static void ModifyResponse(RoutingContext context, HttpResponse Response) + { + var statusCode = context.status.Value; + + // 300-399 + if (statusCode >= 300 && statusCode < 400) + { + if (!string.IsNullOrEmpty(context.url)) + { + if (statusCode == 301) + { +#if NET451 + Response.RedirectPermanent(context.url); +#else + Response.Redirect(context.url, true); +#endif + } + else // 302 and all others + { + Response.Redirect(context.url); + } + } + else + { + throw new ReactRouterException("Router requested redirect but no url provided."); + } + } + else + { + Response.StatusCode = statusCode; + } + } + } +} diff --git a/src/React.sln b/src/React.sln index d5739dc96..12b174455 100644 --- a/src/React.sln +++ b/src/React.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26228.4 +VisualStudioVersion = 15.0.26403.3 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{F567B25C-E869-4C93-9C96-077761250F87}" EndProject @@ -11,6 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Library", "Library", "{681C EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{CB51F03F-49BD-4B79-8AD4-67962230E76B}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig ..\.gitignore = ..\.gitignore ..\build.proj = ..\build.proj ..\dev-build-push.bat = ..\dev-build-push.bat diff --git a/tests/React.Tests/Core/ReactEnvironmentTest.cs b/tests/React.Tests/Core/ReactEnvironmentTest.cs index 8e437a0b3..4a223b90e 100644 --- a/tests/React.Tests/Core/ReactEnvironmentTest.cs +++ b/tests/React.Tests/Core/ReactEnvironmentTest.cs @@ -113,7 +113,7 @@ public void ReturnsEngineToPool() var mocks = new Mocks(); var environment = mocks.CreateReactEnvironment(); mocks.Config.Setup(x => x.ReuseJavaScriptEngines).Returns(true); - + environment.CreateComponent("ComponentName", new { }); mocks.EngineFactory.Verify(x => x.GetEngine(), Times.Once); environment.ReturnEngineToPool(); @@ -168,6 +168,17 @@ public ReactEnvironment CreateReactEnvironment() FileCacheHash.Object ); } + + public Mock CreateMockedReactEnvironment() + { + return new Mock( + EngineFactory.Object, + Config.Object, + Cache.Object, + FileSystem.Object, + FileCacheHash.Object + ); + } } } } diff --git a/tests/React.Tests/Router/HtmlHelperExtensionsTest.cs b/tests/React.Tests/Router/HtmlHelperExtensionsTest.cs index e5d8a2921..6e48e10d4 100644 --- a/tests/React.Tests/Router/HtmlHelperExtensionsTest.cs +++ b/tests/React.Tests/Router/HtmlHelperExtensionsTest.cs @@ -13,11 +13,18 @@ using React.Router; using React.Tests.Core; using System.Web; +using JavaScriptEngineSwitcher.Core; +using System.Web.Mvc; namespace React.Tests.Router { public class HtmlHelperExtensionsTest { + public interface IReactEnvironmentMock : IReactEnvironment + { + IJsEngine Engine { get; set; } + } + /// /// Creates a mock and registers it with the IoC container /// This is only required because can not be @@ -46,79 +53,102 @@ private Mock ConfigureMockConfiguration() return config; } - //[Fact] - //public void EngineIsReturnedToPoolAfterRender() - //{ - // var component = new Mock(); - // component.Setup(x => x.RenderHtml(true, true)).Returns("HTML"); - // var environment = ConfigureMockReactEnvironment(); - // environment.Setup(x => x.CreateComponent( - // "ComponentName", - // new { }, - // null, - // true - // )).Returns(component.Object); - - // environment.Verify(x => x.ReturnEngineToPool(), Times.Never); - // var result = HtmlHelperExtensions.ReactRouterWithContext( - // htmlHelper: null, - // componentName: "ComponentName", - // props: new { }, - // path: "/", - // htmlTag: "span", - // clientOnly: true, - // serverOnly: true - // ); - // component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == true)), Times.Once); - // environment.Verify(x => x.ReturnEngineToPool(), Times.Once); - //} + class HtmlHelperMock + { + public Mock htmlHelper; + public Mock httpResponse; + + public HtmlHelperMock() + { + var viewDataContainer = new Mock(); + var viewContext = new Mock(); + httpResponse = new Mock(); + htmlHelper = new Mock(viewContext.Object, viewDataContainer.Object); + var httpContextBase = new Mock(); + viewContext.Setup(x => x.HttpContext).Returns(httpContextBase.Object); + httpContextBase.Setup(x => x.Response).Returns(httpResponse.Object); + } + } + + [Fact] + public void EngineIsReturnedToPoolAfterRender() + { + ConfigureMockConfiguration(); + + var component = new Mock(); + component.Setup(x => x.RenderHtml(true, true)).Returns("HTML"); + + var environment = ConfigureMockEnvironment(); + environment.Setup(x => x.CreateComponent( + "ComponentName", + new { }, + null, + true + )).Returns(component.Object); + environment.Setup(x => x.Execute("JSON.stringify(context);")) + .Returns("{ }"); + + var htmlHelperMock = new HtmlHelperMock(); + + environment.Verify(x => x.ReturnEngineToPool(), Times.Never); + var result = HtmlHelperExtensions.ReactRouterWithContext( + htmlHelper: htmlHelperMock.htmlHelper.Object, + componentName: "ComponentName", + props: new { }, + path: "/", + htmlTag: "span", + clientOnly: true, + serverOnly: true + ); + environment.Verify(x => x.ReturnEngineToPool(), Times.Once); + } //[Fact] //public void ReactWithClientOnlyTrueShouldCallRenderHtmlWithTrue() //{ - // var component = new Mock(); - // component.Setup(x => x.RenderHtml(true, true)).Returns("HTML"); - // var environment = ConfigureMockEnvironment(); - // environment.Setup(x => x.CreateComponent( - // "ComponentName", - // new { }, - // null, - // true - // )).Returns(component.Object); - - // var result = HtmlHelperExtensions.ReactRouterWithContext( - // htmlHelper: null, - // componentName: "ComponentName", - // props: new { }, - // htmlTag: "span", - // clientOnly: true, - // serverOnly: true - // ); - // component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == true)), Times.Once); + // var component = new Mock(); + // component.Setup(x => x.RenderHtml(true, true)).Returns("HTML"); + // var environment = ConfigureMockEnvironment(); + // environment.Setup(x => x.CreateComponent( + // "ComponentName", + // new { }, + // null, + // true + // )).Returns(component.Object); + + // var result = HtmlHelperExtensions.ReactRouterWithContext( + // htmlHelper: null, + // componentName: "ComponentName", + // props: new { }, + // htmlTag: "span", + // clientOnly: true, + // serverOnly: true + // ); + // component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == true)), Times.Once); //} //[Fact] //public void ReactWithServerOnlyTrueShouldCallRenderHtmlWithTrue() //{ - // var component = new Mock(); - // component.Setup(x => x.RenderHtml(true, true)).Returns("HTML"); - // var environment = ConfigureMockEnvironment(); - // environment.Setup(x => x.CreateComponent( - // "ComponentName", - // new { }, - // null, - // true - // )).Returns(component.Object); - - // var result = HtmlHelperExtensions.React( - // htmlHelper: null, - // componentName: "ComponentName", - // props: new { }, - // htmlTag: "span", - // clientOnly: true, - // serverOnly: true - // ); - // component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == true)), Times.Once); + // var component = new Mock(); + // component.Setup(x => x.RenderHtml(true, true)).Returns("HTML"); + // var environment = ConfigureMockEnvironment(); + // environment.Setup(x => x.CreateComponent( + // "ComponentName", + // new { }, + // null, + // true + // )).Returns(component.Object); + + // var result = HtmlHelperExtensions.React( + // htmlHelper: null, + // componentName: "ComponentName", + // props: new { }, + // htmlTag: "span", + // clientOnly: true, + // serverOnly: true + // ); + // component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == true)), Times.Once); //} [Fact] @@ -130,39 +160,36 @@ public void ShouldModifyStatusCode() mocks.Engine.Setup(x => x.Evaluate("JSON.stringify(context);")) .Returns("{ status: 200 }"); - var httpResponse = new Mock(); + var htmlHelperMock = new HtmlHelperMock(); HtmlHelperExtensions.ReactRouterWithContext( - htmlHelper: null, + htmlHelper: htmlHelperMock.htmlHelper.Object, componentName: "ComponentName", props: new { }, - path: "/", - Response: httpResponse.Object + path: "/" ); - httpResponse.VerifySet(x => x.StatusCode = 200); + htmlHelperMock.httpResponse.VerifySet(x => x.StatusCode = 200); } [Fact] public void ShouldRunCustomContextHandler() { - var mocks = ConfigureMockReactEnvironment(); ConfigureMockConfiguration(); mocks.Engine.Setup(x => x.Evaluate("JSON.stringify(context);")) .Returns("{ status: 200 }"); - var httpResponse = new Mock(); - + var htmlHelperMock = new HtmlHelperMock(); + HtmlHelperExtensions.ReactRouterWithContext( - htmlHelper: null, + htmlHelper: htmlHelperMock.htmlHelper.Object, componentName: "ComponentName", props: new { }, path: "/", - Response: httpResponse.Object, contextHandler: (response, context) => response.StatusCode = context.status.Value ); - httpResponse.VerifySet(x => x.StatusCode = 200); + htmlHelperMock.httpResponse.VerifySet(x => x.StatusCode = 200); } [Fact] @@ -174,16 +201,15 @@ public void ShouldRedirectPermanent() mocks.Engine.Setup(x => x.Evaluate("JSON.stringify(context);")) .Returns(@"{ status: 301, url: ""/foo"" }"); - var httpResponse = new Mock(); + var htmlHelperMock = new HtmlHelperMock(); HtmlHelperExtensions.ReactRouterWithContext( - htmlHelper: null, + htmlHelper: htmlHelperMock.htmlHelper.Object, componentName: "ComponentName", props: new { }, - path: "/", - Response: httpResponse.Object + path: "/" ); - httpResponse.Verify(x => x.RedirectPermanent(It.IsAny())); + htmlHelperMock.httpResponse.Verify(x => x.RedirectPermanent(It.IsAny())); } [Fact] @@ -195,16 +221,15 @@ public void ShouldFailRedirectWithNoUrl() mocks.Engine.Setup(x => x.Evaluate("JSON.stringify(context);")) .Returns("{ status: 301 }"); - var httpResponse = new Mock(); + var htmlHelperMock = new HtmlHelperMock(); Assert.Throws(() => HtmlHelperExtensions.ReactRouterWithContext( - htmlHelper: null, + htmlHelper: htmlHelperMock.htmlHelper.Object, componentName: "ComponentName", props: new { }, - path: "/", - Response: httpResponse.Object + path: "/" ) ); } diff --git a/tests/React.Tests/Router/ReactEnvironmentExtensionsTest.cs b/tests/React.Tests/Router/ReactEnvironmentExtensionsTest.cs index 9df351251..98a486311 100644 --- a/tests/React.Tests/Router/ReactEnvironmentExtensionsTest.cs +++ b/tests/React.Tests/Router/ReactEnvironmentExtensionsTest.cs @@ -17,34 +17,24 @@ namespace React.Tests.Router { public class ReactEnvironmentExtensionsTest { - /// - /// Creates a mock and registers it with the IoC container - /// This is only required because can not be - /// injected :( - /// - private ReactEnvironment ConfigureMockReactEnvironment() + [Fact] + public void EnvironmentShouldGetCalledClientOnly() { - var mocks = new ReactEnvironmentTest.Mocks(); + var environment = new Mock(); + AssemblyRegistration.Container.Register(environment.Object); + var config = new Mock(); + AssemblyRegistration.Container.Register(config.Object); - var environment = mocks.CreateReactEnvironment(); - AssemblyRegistration.Container.Register(environment); - return environment; - } - - //[Fact] - //public void EnvironmentShouldGetCalledClientOnly() - //{ - // var environment = ConfigureMockReactEnvironment(); - // var component = ReactEnvironmentExtensions.CreateRouterComponent( - // environment.Object, - // "ComponentName", - // new { }, - // "/", - // null, - // true - // ); + var component = ReactEnvironmentExtensions.CreateRouterComponent( + environment.Object, + "ComponentName", + new { }, + "/", + null, + true + ); - // environment.Verify(x => x.CreateComponent("ComponentName", new { }, null, true)); - //} - } + environment.Verify(x => x.CreateComponent(It.IsAny(), true)); + } + } } diff --git a/tests/React.Tests/Router/ReactRouterComponentTest.cs b/tests/React.Tests/Router/ReactRouterComponentTest.cs index f44577725..59b90b06a 100644 --- a/tests/React.Tests/Router/ReactRouterComponentTest.cs +++ b/tests/React.Tests/Router/ReactRouterComponentTest.cs @@ -34,25 +34,5 @@ public void RenderJavaScriptShouldNotIncludeContextOrPath() result ); } - - //[Fact] - //public void ShouldUsePolifyfillSuccessfully() - //{ - // var mocks = new ReactEnvironmentTest.Mocks(); - // var environment = mocks.CreateReactEnvironment(); - // //environment.Setup(x => x.Execute("typeof Object.assign === 'function'")).Returns(false); - // //environment.Setup(x => x.Execute("JSON.stringify(context);")).Returns("{}"); - - // var config = new Mock(); - // config.Setup(x => x.UseServerSideRendering).Returns(false); - - // var component = new ReactRouterComponent(environment, config.Object, "Foo", "container", "/bar") - // { - // Props = new { hello = "World" } - // }; - // var html = component.RenderRouterWithContext(true); - // var result = environment.Execute(@"Object.assign(""a"", { sup=""yo""})"); - // Assert.Equal(@"{ [string: ""a""], sup: ""yo""", result); - //} } } From 6d14da03637bf0964c9baba348b1622a234ee9a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunnar=20M=C3=A1r=20=C3=93ttarsson?= Date: Sun, 9 Jul 2017 23:04:26 +0000 Subject: [PATCH 9/9] Finished fixing all unit tests --- .../ReactEnvironmentExtensions.cs | 3 +- .../Router/HtmlHelperExtensionsTest.cs | 165 ++++++++++-------- 2 files changed, 93 insertions(+), 75 deletions(-) diff --git a/src/React.Router/ReactEnvironmentExtensions.cs b/src/React.Router/ReactEnvironmentExtensions.cs index 809580d8e..3137538cd 100644 --- a/src/React.Router/ReactEnvironmentExtensions.cs +++ b/src/React.Router/ReactEnvironmentExtensions.cs @@ -48,8 +48,7 @@ public static ReactRouterComponent CreateRouterComponent( Props = props, }; - env.CreateComponent(component, clientOnly); - return component; + return env.CreateComponent(component, clientOnly) as ReactRouterComponent; } } } diff --git a/tests/React.Tests/Router/HtmlHelperExtensionsTest.cs b/tests/React.Tests/Router/HtmlHelperExtensionsTest.cs index 6e48e10d4..7ccaf6230 100644 --- a/tests/React.Tests/Router/HtmlHelperExtensionsTest.cs +++ b/tests/React.Tests/Router/HtmlHelperExtensionsTest.cs @@ -20,11 +20,6 @@ namespace React.Tests.Router { public class HtmlHelperExtensionsTest { - public interface IReactEnvironmentMock : IReactEnvironment - { - IJsEngine Engine { get; set; } - } - /// /// Creates a mock and registers it with the IoC container /// This is only required because can not be @@ -53,12 +48,16 @@ private Mock ConfigureMockConfiguration() return config; } - class HtmlHelperMock + /// + /// Mock an html helper with a mocked response object. + /// Used when testing for server response modification. + /// + class HtmlHelperMocks { public Mock htmlHelper; public Mock httpResponse; - public HtmlHelperMock() + public HtmlHelperMocks() { var viewDataContainer = new Mock(); var viewContext = new Mock(); @@ -70,25 +69,51 @@ public HtmlHelperMock() } } + /// + /// Mocks alot of common functionality related to rendering a + /// React Router component. + /// + class ReactRouterMocks + { + public Mock config; + public Mock environment; + public Mock component; + + public ReactRouterMocks( + Mock conf, + Mock env + ) + { + config = conf; + environment = env; + + component = new Mock( + environment.Object, + config.Object, + "ComponentName", + "", + "/" + ); + var execResult = new Mock(); + + component.Setup(x => x.RenderRouterWithContext(It.IsAny(), It.IsAny())) + .Returns(execResult.Object); + environment.Setup(x => x.CreateComponent( + It.IsAny(), + It.IsAny() + )).Returns(component.Object); + environment.Setup(x => x.Execute("JSON.stringify(context);")) + .Returns("{ }"); + } + } + [Fact] public void EngineIsReturnedToPoolAfterRender() { - ConfigureMockConfiguration(); - - var component = new Mock(); - component.Setup(x => x.RenderHtml(true, true)).Returns("HTML"); - + var config = ConfigureMockConfiguration(); var environment = ConfigureMockEnvironment(); - environment.Setup(x => x.CreateComponent( - "ComponentName", - new { }, - null, - true - )).Returns(component.Object); - environment.Setup(x => x.Execute("JSON.stringify(context);")) - .Returns("{ }"); - - var htmlHelperMock = new HtmlHelperMock(); + var routerMocks = new ReactRouterMocks(config, environment); + var htmlHelperMock = new HtmlHelperMocks(); environment.Verify(x => x.ReturnEngineToPool(), Times.Never); var result = HtmlHelperExtensions.ReactRouterWithContext( @@ -103,53 +128,47 @@ public void EngineIsReturnedToPoolAfterRender() environment.Verify(x => x.ReturnEngineToPool(), Times.Once); } - //[Fact] - //public void ReactWithClientOnlyTrueShouldCallRenderHtmlWithTrue() - //{ - // var component = new Mock(); - // component.Setup(x => x.RenderHtml(true, true)).Returns("HTML"); - // var environment = ConfigureMockEnvironment(); - // environment.Setup(x => x.CreateComponent( - // "ComponentName", - // new { }, - // null, - // true - // )).Returns(component.Object); - - // var result = HtmlHelperExtensions.ReactRouterWithContext( - // htmlHelper: null, - // componentName: "ComponentName", - // props: new { }, - // htmlTag: "span", - // clientOnly: true, - // serverOnly: true - // ); - // component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == true)), Times.Once); - //} - - //[Fact] - //public void ReactWithServerOnlyTrueShouldCallRenderHtmlWithTrue() - //{ - // var component = new Mock(); - // component.Setup(x => x.RenderHtml(true, true)).Returns("HTML"); - // var environment = ConfigureMockEnvironment(); - // environment.Setup(x => x.CreateComponent( - // "ComponentName", - // new { }, - // null, - // true - // )).Returns(component.Object); - - // var result = HtmlHelperExtensions.React( - // htmlHelper: null, - // componentName: "ComponentName", - // props: new { }, - // htmlTag: "span", - // clientOnly: true, - // serverOnly: true - // ); - // component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == true)), Times.Once); - //} + [Fact] + public void ReactWithClientOnlyTrueShouldCallRenderHtmlWithTrue() + { + var config = ConfigureMockConfiguration(); + + var htmlHelperMock = new HtmlHelperMocks(); + var environment = ConfigureMockEnvironment(); + var routerMocks = new ReactRouterMocks(config, environment); + + var result = HtmlHelperExtensions.ReactRouterWithContext( + htmlHelper: htmlHelperMock.htmlHelper.Object, + componentName: "ComponentName", + props: new { }, + path: "/", + htmlTag: "span", + clientOnly: true, + serverOnly: false + ); + routerMocks.component.Verify(x => x.RenderRouterWithContext(It.Is(y => y == true), It.Is(z => z == false)), Times.Once); + } + + [Fact] + public void ReactWithServerOnlyTrueShouldCallRenderHtmlWithTrue() + { + var config = ConfigureMockConfiguration(); + + var htmlHelperMock = new HtmlHelperMocks(); + var environment = ConfigureMockEnvironment(); + var routerMocks = new ReactRouterMocks(config, environment); + + var result = HtmlHelperExtensions.ReactRouterWithContext( + htmlHelper: htmlHelperMock.htmlHelper.Object, + componentName: "ComponentName", + props: new { }, + path: "/", + htmlTag: "span", + clientOnly: false, + serverOnly: true + ); + routerMocks.component.Verify(x => x.RenderRouterWithContext(It.Is(y => y == false), It.Is(z => z == true)), Times.Once); + } [Fact] public void ShouldModifyStatusCode() @@ -160,7 +179,7 @@ public void ShouldModifyStatusCode() mocks.Engine.Setup(x => x.Evaluate("JSON.stringify(context);")) .Returns("{ status: 200 }"); - var htmlHelperMock = new HtmlHelperMock(); + var htmlHelperMock = new HtmlHelperMocks(); HtmlHelperExtensions.ReactRouterWithContext( htmlHelper: htmlHelperMock.htmlHelper.Object, @@ -180,7 +199,7 @@ public void ShouldRunCustomContextHandler() mocks.Engine.Setup(x => x.Evaluate("JSON.stringify(context);")) .Returns("{ status: 200 }"); - var htmlHelperMock = new HtmlHelperMock(); + var htmlHelperMock = new HtmlHelperMocks(); HtmlHelperExtensions.ReactRouterWithContext( htmlHelper: htmlHelperMock.htmlHelper.Object, @@ -201,7 +220,7 @@ public void ShouldRedirectPermanent() mocks.Engine.Setup(x => x.Evaluate("JSON.stringify(context);")) .Returns(@"{ status: 301, url: ""/foo"" }"); - var htmlHelperMock = new HtmlHelperMock(); + var htmlHelperMock = new HtmlHelperMocks(); HtmlHelperExtensions.ReactRouterWithContext( htmlHelper: htmlHelperMock.htmlHelper.Object, @@ -221,7 +240,7 @@ public void ShouldFailRedirectWithNoUrl() mocks.Engine.Setup(x => x.Evaluate("JSON.stringify(context);")) .Returns("{ status: 301 }"); - var htmlHelperMock = new HtmlHelperMock(); + var htmlHelperMock = new HtmlHelperMocks(); Assert.Throws(() =>