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 4de1430b7..ea5cdb8a2 100644 --- a/build.proj +++ b/build.proj @@ -27,6 +27,7 @@ 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/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.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 97d8cc296..e3a706812 100644 --- a/src/React.Core/ReactEnvironment.cs +++ b/src/React.Core/ReactEnvironment.cs @@ -18,6 +18,7 @@ using JSPool; using Newtonsoft.Json; using React.Exceptions; +using React.TinyIoC; namespace React { @@ -85,6 +86,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. /// @@ -274,6 +303,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.Core/Resources/shims.js b/src/React.Core/Resources/shims.js index 2ac285f21..ba2edb908 100644 --- a/src/React.Core/Resources/shims.js +++ b/src/React.Core/Resources/shims.js @@ -67,3 +67,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/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 new file mode 100644 index 000000000..ade20da8a --- /dev/null +++ b/src/React.Router/ExecutionResult.cs @@ -0,0 +1,28 @@ +/* + * 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; } + + /// + /// 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..cabd14c6f --- /dev/null +++ b/src/React.Router/HtmlHelperExtensions.cs @@ -0,0 +1,119 @@ +/* + * 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; + +#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 + { + 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. + /// 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, + string htmlTag = null, + string containerId = null, + bool clientOnly = false, + bool serverOnly = false, + string containerClass = null, + Action contextHandler = null + ) + { + try + { + var response = htmlHelper.ViewContext.HttpContext.Response; + path = path ?? htmlHelper.ViewContext.HttpContext.Request.Path; + + var reactComponent + = Environment.CreateRouterComponent( + componentName, + props, + path, + containerId, + clientOnly + ); + + if (!string.IsNullOrEmpty(htmlTag)) + { + reactComponent.ContainerTag = htmlTag; + } + if (!string.IsNullOrEmpty(containerClass)) + { + reactComponent.ContainerClass = containerClass; + } + + var executionResult = reactComponent.RenderRouterWithContext(clientOnly, serverOnly); + + if (executionResult.Context?.status != null) + { + // Use provided contextHandler + if (contextHandler != null) + { + contextHandler(response, executionResult.Context); + } + // Handle routing context internally + else + { + SetServerResponse.ModifyResponse(executionResult.Context, response); + } + } + + return new HtmlString(executionResult.RenderResult); + } + finally + { + Environment.ReturnEngineToPool(); + } + } + } +} 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..c03204a80 --- /dev/null +++ b/src/React.Router/React.Router.csproj @@ -0,0 +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;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/ReactEnvironmentExtensions.cs b/src/React.Router/ReactEnvironmentExtensions.cs new file mode 100644 index 000000000..3137538cd --- /dev/null +++ b/src/React.Router/ReactEnvironmentExtensions.cs @@ -0,0 +1,54 @@ +/* + * 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. + /// + /// Type of the props + /// 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, + string path, + string containerId = null, + bool clientOnly = false + ) + { + var config = AssemblyRegistration.Container.Resolve(); + + var component = new ReactRouterComponent( + env, + config, + componentName, + containerId, + path + ) + { + Props = props, + }; + + return env.CreateComponent(component, clientOnly) as ReactRouterComponent; + } + } +} diff --git a/src/React.Router/ReactRouterComponent.cs b/src/React.Router/ReactRouterComponent.cs new file mode 100644 index 000000000..c2d553027 --- /dev/null +++ b/src/React.Router/ReactRouterComponent.cs @@ -0,0 +1,98 @@ +/* + * 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; + +namespace React.Router +{ + /// + /// Represents a React Router JavaScript component. + /// + public class ReactRouterComponent : ReactComponent + { + /// + /// 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 path + ) : base(environment, configuration, componentName, containerId) + { + _path = path; + } + + /// + /// Render a React StaticRouter Component with context object. + /// + /// 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 virtual ExecutionResult RenderRouterWithContext(bool renderContainerOnly = false, bool renderServerOnly = false) + { + _environment.Execute("var context = {};"); + + var html = RenderHtml(renderContainerOnly, renderServerOnly); + + var contextString = _environment.Execute("JSON.stringify(context);"); + + return new ExecutionResult + { + RenderResult = html, + Context = JsonConvert.DeserializeObject(contextString), + }; + } + + /// + /// 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 + ); + } + + /// + /// 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 + ); + } + } +} diff --git a/src/React.Router/ReactRouterException.cs b/src/React.Router/ReactRouterException.cs new file mode 100644 index 000000000..419397be4 --- /dev/null +++ b/src/React.Router/ReactRouterException.cs @@ -0,0 +1,50 @@ +/* + * 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 +{ + /// + /// 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..49e00ca9e --- /dev/null +++ b/src/React.Router/RoutingContext.cs @@ -0,0 +1,29 @@ +/* + * 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; } + + /// + /// 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.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 e34a4eeb5..db8d048dc 100644 --- a/src/React.sln +++ b/src/React.sln @@ -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 @@ -63,6 +64,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 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "React.AspNet.Middleware", "React.AspNet.Middleware\React.AspNet.Middleware.csproj", "{7E1C3999-1982-476D-9307-12B30737B41E}" EndProject Global @@ -131,6 +134,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 {7E1C3999-1982-476D-9307-12B30737B41E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7E1C3999-1982-476D-9307-12B30737B41E}.Debug|Any CPU.Build.0 = Debug|Any CPU {7E1C3999-1982-476D-9307-12B30737B41E}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -155,6 +162,7 @@ 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} {7E1C3999-1982-476D-9307-12B30737B41E} = {681C45FB-103C-48BC-B992-20C5B6B78F92} EndGlobalSection EndGlobal diff --git a/tests/React.Tests/Core/ReactEnvironmentTest.cs b/tests/React.Tests/Core/ReactEnvironmentTest.cs index 5829d21d2..4017051ba 100644 --- a/tests/React.Tests/Core/ReactEnvironmentTest.cs +++ b/tests/React.Tests/Core/ReactEnvironmentTest.cs @@ -123,7 +123,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; } @@ -155,6 +169,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/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/React.Tests.csproj b/tests/React.Tests/React.Tests.csproj index 662774500..8f21419d2 100644 --- a/tests/React.Tests/React.Tests.csproj +++ b/tests/React.Tests/React.Tests.csproj @@ -18,6 +18,7 @@ + diff --git a/tests/React.Tests/Router/HtmlHelperExtensionsTest.cs b/tests/React.Tests/Router/HtmlHelperExtensionsTest.cs new file mode 100644 index 000000000..7ccaf6230 --- /dev/null +++ b/tests/React.Tests/Router/HtmlHelperExtensionsTest.cs @@ -0,0 +1,256 @@ +/* + * 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; +using JavaScriptEngineSwitcher.Core; +using System.Web.Mvc; + +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; + } + + /// + /// 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 HtmlHelperMocks() + { + 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); + } + } + + /// + /// 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() + { + var config = ConfigureMockConfiguration(); + var environment = ConfigureMockEnvironment(); + var routerMocks = new ReactRouterMocks(config, environment); + var htmlHelperMock = new HtmlHelperMocks(); + + 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 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() + { + var mocks = ConfigureMockReactEnvironment(); + ConfigureMockConfiguration(); + + mocks.Engine.Setup(x => x.Evaluate("JSON.stringify(context);")) + .Returns("{ status: 200 }"); + + var htmlHelperMock = new HtmlHelperMocks(); + + HtmlHelperExtensions.ReactRouterWithContext( + htmlHelper: htmlHelperMock.htmlHelper.Object, + componentName: "ComponentName", + props: new { }, + path: "/" + ); + 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 htmlHelperMock = new HtmlHelperMocks(); + + HtmlHelperExtensions.ReactRouterWithContext( + htmlHelper: htmlHelperMock.htmlHelper.Object, + componentName: "ComponentName", + props: new { }, + path: "/", + contextHandler: (response, context) => response.StatusCode = context.status.Value + ); + htmlHelperMock.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 htmlHelperMock = new HtmlHelperMocks(); + + HtmlHelperExtensions.ReactRouterWithContext( + htmlHelper: htmlHelperMock.htmlHelper.Object, + componentName: "ComponentName", + props: new { }, + path: "/" + ); + htmlHelperMock.httpResponse.Verify(x => x.RedirectPermanent(It.IsAny())); + } + + [Fact] + public void ShouldFailRedirectWithNoUrl() + { + var mocks = ConfigureMockReactEnvironment(); + ConfigureMockConfiguration(); + + mocks.Engine.Setup(x => x.Evaluate("JSON.stringify(context);")) + .Returns("{ status: 301 }"); + + var htmlHelperMock = new HtmlHelperMocks(); + + Assert.Throws(() => + + HtmlHelperExtensions.ReactRouterWithContext( + htmlHelper: htmlHelperMock.htmlHelper.Object, + componentName: "ComponentName", + props: new { }, + path: "/" + ) + ); + } + } +} diff --git a/tests/React.Tests/Router/ReactEnvironmentExtensionsTest.cs b/tests/React.Tests/Router/ReactEnvironmentExtensionsTest.cs new file mode 100644 index 000000000..98a486311 --- /dev/null +++ b/tests/React.Tests/Router/ReactEnvironmentExtensionsTest.cs @@ -0,0 +1,40 @@ +/* + * 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(); + AssemblyRegistration.Container.Register(environment.Object); + var config = new Mock(); + AssemblyRegistration.Container.Register(config.Object); + + var component = ReactEnvironmentExtensions.CreateRouterComponent( + environment.Object, + "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 new file mode 100644 index 000000000..59b90b06a --- /dev/null +++ b/tests/React.Tests/Router/ReactRouterComponentTest.cs @@ -0,0 +1,38 @@ +/* + * 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 ReactRouterComponentTest + { + [Fact] + public void RenderJavaScriptShouldNotIncludeContextOrPath() + { + var environment = new Mock(); + var config = new Mock(); + + var component = new ReactRouterComponent(environment.Object, config.Object, "Foo", "container", "/bar") + { + Props = new { hello = "World" } + }; + var result = component.RenderJavaScript(); + + Assert.Equal( + @"ReactDOM.render(React.createElement(Foo, {""hello"":""World""}), document.getElementById(""container""))", + result + ); + } + } +}