├── .formatter.exs ├── .gitignore ├── LICENSE.md ├── README.md ├── TODO.md ├── assets ├── babel.config.js ├── jest.config.js ├── js │ ├── browser.js │ ├── component.js │ ├── dom.js │ └── presto.js ├── package.json ├── tests │ ├── browser.test.js │ ├── component.test.js │ ├── dom.test.js │ ├── presto.test.js │ └── test_helpers.js ├── webpack.config.js └── yarn.lock ├── config └── config.exs ├── lib ├── presto.ex └── presto │ ├── action.ex │ ├── application.ex │ ├── channel.ex │ ├── component.ex │ ├── component_supervisor.ex │ └── util.ex ├── mix.exs ├── mix.lock ├── package.json ├── priv └── static │ ├── presto.js │ └── prestoTest.js └── test ├── presto ├── component_supervisor_test.exs └── component_test.exs ├── presto_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | presto-*.tar 24 | 25 | node_modules 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Apache License 2 | **Version 2.0, January 2004** 3 | 4 | [**http://www.apache.org/licenses/**](http://www.apache.org/licenses/) 5 | 6 | ## TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | #### 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 16 | 17 | "You" (or "Your") shall mean an individual or Legal Entity 18 | exercising permissions granted by this License. 19 | 20 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 21 | 22 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 23 | 24 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 25 | 26 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 27 | 28 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 29 | 30 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 31 | 32 | #### 2. Grant of Copyright License. 33 | 34 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 35 | 36 | #### 3. Grant of Patent License. 37 | 38 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 39 | 40 | #### 4. Redistribution. 41 | 42 | You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 43 | 44 | **(a)** You must give any other recipients of the Work or Derivative Works a copy of this License; and 45 | 46 | **(b)** You must cause any modified files to carry prominent notices stating that You changed the files; and 47 | 48 | **(c)** You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 49 | 50 | **(d)** If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 51 | 52 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 53 | 54 | #### 5. Submission of Contributions. 55 | 56 | Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 57 | 58 | #### 6. Trademarks. 59 | 60 | This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 61 | 62 | #### 7. Disclaimer of Warranty. 63 | 64 | Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 65 | 66 | #### 8. Limitation of Liability. 67 | 68 | In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 69 | 70 | #### 9. Accepting Warranty or Additional Liability. 71 | 72 | While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 73 | 74 | ***END OF TERMS AND CONDITIONS*** 75 | * * * 76 | 77 | #### APPENDIX: How to apply the Apache License to your work. 78 | 79 | To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. 80 | 81 |
82 | Copyright [yyyy] [name of copyright owner]
83 | 
84 | Licensed under the Apache License, Version 2.0 (the "License");
85 | you may not use this file except in compliance with the License.
86 | You may obtain a copy of the License at
87 | 
88 |     http://www.apache.org/licenses/LICENSE-2.0
89 | 
90 | Unless required by applicable law or agreed to in writing, software
91 | distributed under the License is distributed on an "AS IS" BASIS,
92 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
93 | See the License for the specific language governing permissions and
94 | limitations under the License.
95 | 
96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Presto 2 | 3 | Presto is an Elixir library for creating Elm-like or React-like single page applications (SPAs) **completely in Elixir**. 4 | 5 | It was presented at ElixirConfEU 2018. You can find the [slides here](https://www.slideshare.net/ijcd/elixirconfeu-2018-spas-without-the-spa). 6 | 7 | ## Installation 8 | 9 | Add this to `mix.exs`: 10 | 11 | ``` 12 | {:presto, "~> 0.1.2"} 13 | ``` 14 | 15 | ## Philosophy 16 | 17 | Web development is too complciated. Front-ends, back-ends, multiple languages, markup, it's all too complicated. Things can be simpler. 18 | 19 | We want: 20 | 21 | 1. the feel and data model (mostly) of React. 22 | 2. views to be a projection of the data. 23 | 3. the simplicity of Elm's model/update/view functions. 24 | 4. all of this in Elixir. 25 | 26 | `Model` -> `Update` -> `View`
27 | `State` -> `Message` -> `Response` 28 | 29 | This is a `GenServer`. 30 | 31 | ## How It Works 32 | 33 | 1. A `GenServer` keeps the state for the user. It’s all on the server. 34 | 2. For a single component root, there is one `GenServer` that comes to life when it gets a message. 35 | 3. It receives DOM events from the browser over a `channel`, updating the `GenServer` state. 36 | 4. UI updates are returned via the `channel`. 37 | 38 | The `GenServers` are managed by a `DynamicSupervisor`. 39 | 40 | Components are scoped to a `visitor_id`, which is unique to each browser. 41 | 42 | ### Add Presto to `mix.exs` 43 | 44 | mix.exs 45 | ```elixir 46 | defp deps do 47 | [ 48 | ... 49 | {:presto, "~> 0.1.2"}, 50 | ... 51 | ] 52 | end 53 | ``` 54 | 55 | ### Create a component 56 | 57 | lib/presto/single_counter.ex 58 | ```elixir 59 | defmodule PrestoDemoWeb.Presto.SingleCounter do 60 | use Presto.Component 61 | use Taggart.HTML 62 | require Logger 63 | 64 | @impl Presto.Component 65 | def initial_model(_model) do 66 | 0 67 | end 68 | 69 | @impl Presto.Component 70 | def update(message, model) do 71 | case message do 72 | %{"event" => "click", "id" => "inc"} -> 73 | model + 1 74 | 75 | %{"event" => "click", "id" => "dec"} -> 76 | model - 1 77 | end 78 | end 79 | 80 | @impl Presto.Component 81 | def render(model) do 82 | div do 83 | "Counter is: #{inspect(model)}" 84 | 85 | button(id: "inc", class: "presto-click") do 86 | "More" 87 | end 88 | 89 | button(id: "dec", class: "presto-click") do 90 | "Less" 91 | end 92 | end 93 | end 94 | end 95 | ``` 96 | 97 | ### Add the component to a view 98 | 99 | index.html.eex 100 | ```elixir 101 | <%= Presto.render(Presto.component(PrestoDemoWeb.Presto.SingleCounter, assigns[:visitor_id])) %> 102 | ``` 103 | 104 | ### Wire up the javascript 105 | 106 | assets/package.json 107 | ```javascript 108 | ... 109 | "dependencies": { 110 | ... 111 | "presto": "file:../deps/presto" 112 | }, 113 | ... 114 | ``` 115 | 116 | app.js 117 | ```javascript 118 | import {Presto} from "presto" 119 | import unpoly from "unpoly/dist/unpoly.js" 120 | let presto = new Presto(channel, up); 121 | ``` 122 | 123 | ### Wire Up A Presto Channel 124 | 125 | user_socker.ex 126 | ```elixir 127 | defmodule PrestoDemoWeb.UserSocket do 128 | use Phoenix.Socket 129 | 130 | channel("presto:*", PrestoDemoWeb.CounterChannel) 131 | 132 | def connect(%{"token" => token} = _params, socket) do 133 | case PrestoDemoWeb.Session.decode_socket_token(token) do 134 | {:ok, visitor_id} -> 135 | {:ok, assign(socket, :visitor_id, visitor_id)} 136 | 137 | {:error, _reason} -> 138 | :error 139 | end 140 | end 141 | ... 142 | ``` 143 | 144 | component_channel.ex 145 | ```elixir 146 | defmodule PrestoDemoWeb.CounterChannel do 147 | ... 148 | def handle_in("presto", payload, socket) do 149 | %{visitor_id: visitor_id} = socket.assigns 150 | 151 | # send event to presto component 152 | {:ok, dispatch} = Presto.dispatch(PrestoDemoWeb.Presto.SingleCounter, visitor_id, payload) 153 | 154 | case dispatch do 155 | [] -> nil 156 | _ -> push(socket, "presto", dispatch) 157 | end 158 | 159 | {:reply, {:ok, payload}, socket} 160 | end 161 | ... 162 | end 163 | ``` 164 | 165 | ### Setup user_token and visitor_id plugs 166 | 167 | router.ex 168 | ```elixir 169 | pipeline :browser do 170 | plug(:accepts, ["html"]) 171 | plug(:fetch_session) 172 | plug(:fetch_flash) 173 | plug(:protect_from_forgery) 174 | plug(:put_secure_browser_headers) 175 | plug(PrestoDemoWeb.Plugs.VisitorIdPlug) 176 | plug(PrestoDemoWeb.Plugs.UserTokenPlug) 177 | end 178 | ``` 179 | 180 | user_token_plug.ex 181 | ```elixir 182 | defmodule PrestoDemoWeb.Plugs.UserTokenPlug do 183 | import Plug.Conn 184 | 185 | def init(default), do: default 186 | 187 | def call(conn, _default) do 188 | if visitor_id = conn.assigns[:visitor_id] do 189 | user_token = PrestoDemoWeb.Session.encode_socket_token(visitor_id) 190 | assign(conn, :user_token, user_token) 191 | else 192 | conn 193 | end 194 | end 195 | end 196 | ``` 197 | 198 | visitor_id_plug.ex 199 | ```elixir 200 | defmodule PrestoDemoWeb.Plugs.VisitorIdPlug do 201 | import Plug.Conn 202 | 203 | @key :visitor_id 204 | 205 | def init(default), do: default 206 | 207 | def call(conn, _default) do 208 | visitor_id = get_session(conn, @key) 209 | 210 | if visitor_id do 211 | assign(conn, @key, visitor_id) 212 | else 213 | visitor_id = Base.encode64(:crypto.strong_rand_bytes(32)) 214 | 215 | conn 216 | |> put_session(@key, visitor_id) 217 | |> assign(@key, visitor_id) 218 | end 219 | end 220 | end 221 | ``` 222 | 223 | ## Testing 224 | 225 | Testing is easy. It’s just a `GenServer`. Spin them up, update, test the response. Done. 226 | 227 | ## Growing Your Application 228 | 229 | Use the language. Growing your app is very simple with this approach. If your `render()` method gets too big, you just split it up in to helpers and modules and whatnot. If your `update()` method gets too big, you just split it up in to helpers and modules and whatnot. 230 | 231 | 232 | ## Demos 233 | 234 | ### Simple Counter 235 | 236 | Here is the code for a [simple counter demo](https://github.com/ijcd/presto_demo) 237 | 238 | ### PrestoChange.io 239 | 240 | This is a real application using `Presto`. 241 | 242 | The code is [here](https://github.com/ijcd/prestochange). 243 | 244 | This is running on the West Coast of the USA: 245 | 246 | [www.prestochange.io](https://www.prestochange.io) 247 | 248 | This is running in Central Europe: 249 | 250 | [eu.prestochange.io](https://eu.prestochange.io) 251 | 252 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | [ ] Implement drab-example demos in presto 4 | [ ] Implement TodoMVC in presto -------------------------------------------------------------------------------- /assets/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | "@babel/preset-env" 4 | ], 5 | env: { 6 | test: { 7 | presets: [ 8 | [ 9 | "@babel/preset-env", 10 | { 11 | targets: { 12 | node: "10" 13 | } 14 | } 15 | ] 16 | ] 17 | } 18 | } 19 | }; -------------------------------------------------------------------------------- /assets/jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // The directory where Jest should store its cached dependency information 12 | // cacheDirectory: "/private/var/folders/f8/6cjdbyz50g59qwby_fvnxr3m0000gp/T/jest_dy", 13 | 14 | // Automatically clear mock calls and instances between every test 15 | // clearMocks: false, 16 | 17 | // Indicates whether the coverage information should be collected while executing the test 18 | // collectCoverage: false, 19 | 20 | // An array of glob patterns indicating a set of files for which coverage information should be collected 21 | // collectCoverageFrom: undefined, 22 | 23 | // The directory where Jest should output its coverage files 24 | coverageDirectory: "coverage", 25 | 26 | // An array of regexp pattern strings used to skip coverage collection 27 | // coveragePathIgnorePatterns: [ 28 | // "/node_modules/" 29 | // ], 30 | 31 | // A list of reporter names that Jest uses when writing coverage reports 32 | // coverageReporters: [ 33 | // "json", 34 | // "text", 35 | // "lcov", 36 | // "clover" 37 | // ], 38 | 39 | // An object that configures minimum threshold enforcement for coverage results 40 | // coverageThreshold: undefined, 41 | 42 | // A path to a custom dependency extractor 43 | // dependencyExtractor: undefined, 44 | 45 | // Make calling deprecated APIs throw helpful error messages 46 | // errorOnDeprecated: false, 47 | 48 | // Force coverage collection from ignored files using an array of glob patterns 49 | // forceCoverageMatch: [], 50 | 51 | // A path to a module which exports an async function that is triggered once before all test suites 52 | // globalSetup: undefined, 53 | 54 | // A path to a module which exports an async function that is triggered once after all test suites 55 | // globalTeardown: undefined, 56 | 57 | // A set of global variables that need to be available in all test environments 58 | // globals: {}, 59 | 60 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 61 | // maxWorkers: "50%", 62 | 63 | // An array of directory names to be searched recursively up from the requiring module's location 64 | // moduleDirectories: [ 65 | // "node_modules" 66 | // ], 67 | 68 | // An array of file extensions your modules use 69 | // moduleFileExtensions: [ 70 | // "js", 71 | // "json", 72 | // "jsx", 73 | // "ts", 74 | // "tsx", 75 | // "node" 76 | // ], 77 | 78 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 79 | // moduleNameMapper: {}, 80 | 81 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 82 | // modulePathIgnorePatterns: [], 83 | 84 | // Activates notifications for test results 85 | // notify: false, 86 | 87 | // An enum that specifies notification mode. Requires { notify: true } 88 | // notifyMode: "failure-change", 89 | 90 | // A preset that is used as a base for Jest's configuration 91 | // preset: undefined, 92 | 93 | // Run tests from one or more projects 94 | // projects: undefined, 95 | 96 | // Use this configuration option to add custom reporters to Jest 97 | // reporters: undefined, 98 | 99 | // Automatically reset mock state between every test 100 | // resetMocks: false, 101 | 102 | // Reset the module registry before running each individual test 103 | // resetModules: false, 104 | 105 | // A path to a custom resolver 106 | // resolver: undefined, 107 | 108 | // Automatically restore mock state between every test 109 | // restoreMocks: false, 110 | 111 | // The root directory that Jest should scan for tests and modules within 112 | // rootDir: undefined, 113 | 114 | // A list of paths to directories that Jest should use to search for files in 115 | // roots: [ 116 | // "" 117 | // ], 118 | 119 | // Allows you to use a custom runner instead of Jest's default test runner 120 | // runner: "jest-runner", 121 | 122 | // The paths to modules that run some code to configure or set up the testing environment before each test 123 | // setupFiles: [], 124 | 125 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 126 | // setupFilesAfterEnv: [], 127 | 128 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 129 | // snapshotSerializers: [], 130 | 131 | // The test environment that will be used for testing 132 | testEnvironment: "jsdom", 133 | 134 | // Options that will be passed to the testEnvironment 135 | // testEnvironmentOptions: {}, 136 | 137 | // Adds a location field to test results 138 | // testLocationInResults: false, 139 | 140 | // The glob patterns Jest uses to detect test files 141 | // testMatch: [ 142 | // "**/__tests__/**/*.[jt]s?(x)", 143 | // "**/?(*.)+(spec|test).[tj]s?(x)" 144 | // ], 145 | 146 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 147 | // testPathIgnorePatterns: [ 148 | // "/node_modules/" 149 | // ], 150 | 151 | // The regexp pattern or array of patterns that Jest uses to detect test files 152 | // testRegex: [], 153 | 154 | // This option allows the use of a custom results processor 155 | // testResultsProcessor: undefined, 156 | 157 | // This option allows use of a custom test runner 158 | // testRunner: "jasmine2", 159 | 160 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 161 | // testURL: "http://localhost", 162 | 163 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 164 | // timers: "real", 165 | 166 | // A map from regular expressions to paths to transformers 167 | // transform: undefined, 168 | 169 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 170 | // transformIgnorePatterns: [ 171 | // "/node_modules/" 172 | // ], 173 | 174 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 175 | // unmockedModulePathPatterns: undefined, 176 | 177 | // Indicates whether each individual test should be reported during the run 178 | // verbose: undefined, 179 | 180 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 181 | // watchPathIgnorePatterns: [], 182 | 183 | // Whether to use watchman for file crawling 184 | // watchman: true, 185 | }; 186 | -------------------------------------------------------------------------------- /assets/js/browser.js: -------------------------------------------------------------------------------- 1 | export let Browser = { 2 | canPushState() { return (typeof (history.pushState) !== "undefined") }, 3 | 4 | dropLocal(namespace, subkey) { 5 | return window.localStorage.removeItem(this.localKey(namespace, subkey)) 6 | }, 7 | 8 | updateLocal(namespace, subkey, initial, func) { 9 | let current = this.getLocal(namespace, subkey) 10 | let key = this.localKey(namespace, subkey) 11 | let newVal = current === null ? initial : func(current) 12 | window.localStorage.setItem(key, JSON.stringify(newVal)) 13 | return newVal 14 | }, 15 | 16 | getLocal(namespace, subkey) { 17 | return JSON.parse(window.localStorage.getItem(this.localKey(namespace, subkey))) 18 | }, 19 | 20 | fetchPage(href, callback) { 21 | let req = new XMLHttpRequest() 22 | req.open("GET", href, true) 23 | req.timeout = PUSH_TIMEOUT 24 | req.setRequestHeader("content-type", "text/html") 25 | req.setRequestHeader("cache-control", "max-age=0, no-cache, no-store, must-revalidate, post-check=0, pre-check=0") 26 | req.setRequestHeader(LINK_HEADER, "live-link") 27 | req.onerror = () => callback(400) 28 | req.ontimeout = () => callback(504) 29 | req.onreadystatechange = () => { 30 | if (req.readyState !== 4) { return } 31 | let requestURL = new URL(href) 32 | let requestPath = requestURL.pathname + requestURL.search 33 | let responseURL = maybe(req.getResponseHeader(RESPONSE_URL_HEADER) || req.responseURL, url => new URL(url)) 34 | let responsePath = responseURL ? responseURL.pathname + responseURL.search : null 35 | if (req.getResponseHeader(LINK_HEADER) !== "live-link") { 36 | return callback(400) 37 | } else if (responseURL === null || responsePath != requestPath) { 38 | return callback(302) 39 | } else if (req.status !== 200) { 40 | return callback(req.status) 41 | } else { 42 | callback(200, req.responseText) 43 | } 44 | } 45 | req.send() 46 | }, 47 | 48 | pushState(kind, meta, to) { 49 | if (this.canPushState()) { 50 | if (to !== window.location.href) { 51 | history[kind + "State"](meta, "", to || null) // IE will coerce undefined to string 52 | let hashEl = this.getHashTargetEl(window.location.hash) 53 | 54 | if (hashEl) { 55 | hashEl.scrollIntoView() 56 | } else if (meta.type === "redirect") { 57 | window.scroll(0, 0) 58 | } 59 | } 60 | } else { 61 | this.redirect(to) 62 | } 63 | }, 64 | 65 | setCookie(name, value) { 66 | document.cookie = `${name}=${value}` 67 | }, 68 | 69 | getCookie(name) { 70 | return document.cookie.replace(new RegExp(`(?:(?:^|.*;\s*)${name}\s*\=\s*([^;]*).*$)|^.*$`), "$1") 71 | }, 72 | 73 | redirect(toURL, flash) { 74 | if (flash) { Browser.setCookie("__phoenix_flash__", flash + "; max-age=60000; path=/") } 75 | window.location = toURL 76 | }, 77 | 78 | localKey(namespace, subkey) { return `${namespace}-${subkey}` }, 79 | 80 | getHashTargetEl(hash) { 81 | if (hash.toString() === "") { return } 82 | return document.getElementById(hash) || document.querySelector(`a[name="${hash.substring(1)}"]`) 83 | } 84 | } -------------------------------------------------------------------------------- /assets/js/component.js: -------------------------------------------------------------------------------- 1 | import morphdom from "morphdom" 2 | import $ from 'cash-dom'; 3 | 4 | export class Component { 5 | 6 | /** 7 | * returns a map, indexing .presto.component-instance elements 8 | * by their corresponding .presto-component#id (component-id) 9 | */ 10 | static scan() { 11 | var m = new Map(); 12 | 13 | // find all component instances in the page 14 | var elements = document.querySelectorAll('.presto-component-instance'); 15 | Array.prototype.forEach.call(elements, function (pci, i) { 16 | // find the component for this instance (should only be one) 17 | var pcc = pci.querySelector('.presto-component') 18 | 19 | // get the current instance set, initializing if not yet found 20 | var instances = m.get(pcc.id); 21 | if (!instances) { 22 | instances = new Set(); 23 | } 24 | 25 | // add instance to instances set, 26 | instances.add(pci.id); 27 | m.set(pcc.id, instances); 28 | }); 29 | 30 | return m; 31 | } 32 | 33 | static update(componentId, content) { 34 | var focused = document.activeElement; 35 | try { 36 | Component.doUpdate(componentId, content); 37 | } 38 | finally { 39 | focused.focus(); 40 | } 41 | } 42 | 43 | // TODO: what is the instanceID stuff all about again? I don't recall... 44 | // I think maybe it was something to do with the way up.extract could only update children of the root node? 45 | static doUpdate(componentId, content) { 46 | // TODO: implement this by listening for DOM mutation events instead (don't scan every time) 47 | var components = Component.scan(); 48 | var instances = components.get(componentId) 49 | 50 | switch (instances) { 51 | case undefined: 52 | console.warn("[Presto] Ignoring request to update unknown componentId: " + componentId); 53 | break; 54 | default: 55 | for (var instanceId of components.get(componentId)) { 56 | var decorated = `
` + content + '
' 57 | var targetNode = $(`div.presto-component-instance#${instanceId}`)[0]; 58 | morphdom(targetNode, decorated); 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /assets/js/dom.js: -------------------------------------------------------------------------------- 1 | const PRESTO_COMPONENT = "data-presto-component" 2 | 3 | export let DOM = { 4 | // byId(id) { return document.getElementById(id) || logError(`no id found for ${id}`) }, 5 | 6 | // removeClass(el, className) { 7 | // el.classList.remove(className) 8 | // if (el.classList.length === 0) { el.removeAttribute("class") } 9 | // }, 10 | 11 | all(node, query, callback) { 12 | let array = Array.from(node.querySelectorAll(query)) 13 | return callback ? array.forEach(callback) : array 14 | }, 15 | 16 | /** 17 | * Returns the first node with cid ID 18 | */ 19 | findFirstComponentNode(node, cid) { 20 | return node.querySelector(`[${PRESTO_COMPONENT}="${cid}"]`) 21 | }, 22 | 23 | // findComponentNodeList(node, cid) { return this.all(node, `[${PRESTO_COMPONENT}="${cid}"]`) }, 24 | 25 | // findPhxChildrenInFragment(html, parentId) { 26 | // let template = document.createElement("template") 27 | // template.innerHTML = html 28 | // return this.findPhxChildren(template.content, parentId) 29 | // }, 30 | 31 | // isPhxUpdate(el, phxUpdate, updateTypes) { 32 | // return el.getAttribute && updateTypes.indexOf(el.getAttribute(phxUpdate)) >= 0 33 | // }, 34 | 35 | // findPhxChildren(el, parentId) { 36 | // return this.all(el, `${PRESTO_VIEW_SELECTOR}[${PRESTO_PARENT_ID}="${parentId}"]`) 37 | // }, 38 | 39 | /** 40 | * Returns all given cids unless they have parents 41 | */ 42 | findParentCIDs(node, cids) { 43 | let initial = new Set(cids) 44 | return cids.reduce((acc, cid) => { 45 | let selector = `[${PRESTO_COMPONENT}="${cid}"] [${PRESTO_COMPONENT}]` 46 | this.all(node, selector) 47 | .map(el => parseInt(el.getAttribute(PRESTO_COMPONENT))) 48 | .forEach(childCID => acc.delete(childCID)) 49 | 50 | return acc 51 | }, initial) 52 | }, 53 | 54 | // private(el, key) { return el[PRESTO_PRIVATE] && el[PRESTO_PRIVATE][key] }, 55 | 56 | // deletePrivate(el, key) { el[PRESTO_PRIVATE] && delete (el[PRESTO_PRIVATE][key]) }, 57 | 58 | // putPrivate(el, key, value) { 59 | // if (!el[PRESTO_PRIVATE]) { el[PRESTO_PRIVATE] = {} } 60 | // el[PRESTO_PRIVATE][key] = value 61 | // }, 62 | 63 | // copyPrivates(target, source) { 64 | // if (source[PRESTO_PRIVATE]) { 65 | // target[PRESTO_PRIVATE] = clone(source[PRESTO_PRIVATE]) 66 | // } 67 | // }, 68 | 69 | // TODO: what are prefix/suffix for? 70 | putTitle(str) { 71 | let titleEl = document.querySelector("title") 72 | let { prefix, suffix } = titleEl.dataset 73 | document.title = `${prefix || ""}${str}${suffix || ""}` 74 | }, 75 | 76 | // debounce(el, event, phxDebounce, defaultDebounce, phxThrottle, defaultThrottle, callback) { 77 | // let debounce = el.getAttribute(phxDebounce) 78 | // let throttle = el.getAttribute(phxThrottle) 79 | // if (debounce === "") { debounce = defaultDebounce } 80 | // if (throttle === "") { throttle = defaultThrottle } 81 | // let value = debounce || throttle 82 | // switch (value) { 83 | // case null: return callback() 84 | 85 | // case "blur": 86 | // if (this.once(el, "debounce-blur")) { 87 | // el.addEventListener("blur", () => callback()) 88 | // } 89 | // return 90 | 91 | // default: 92 | // let timeout = parseInt(value) 93 | // let trigger = () => throttle ? this.deletePrivate(el, THROTTLED) : callback() 94 | // let currentCycle = this.incCycle(el, DEBOUNCE_TRIGGER, trigger) 95 | // if (isNaN(timeout)) { return logError(`invalid throttle/debounce value: ${value}`) } 96 | // if (throttle) { 97 | // if (event.type === "keydown") { 98 | // let prevKey = this.private(el, DEBOUNCE_PREV_KEY) 99 | // this.putPrivate(el, DEBOUNCE_PREV_KEY, event.which) 100 | // if (prevKey !== event.which) { return callback() } 101 | // } else if (this.private(el, THROTTLED)) { 102 | // return false 103 | // } else { 104 | // callback() 105 | // this.putPrivate(el, THROTTLED, true) 106 | // setTimeout(() => this.triggerCycle(el, DEBOUNCE_TRIGGER), timeout) 107 | // } 108 | // } else { 109 | // setTimeout(() => this.triggerCycle(el, DEBOUNCE_TRIGGER, currentCycle), timeout) 110 | // } 111 | 112 | // if (el.form && this.once(el.form, "bind-debounce")) { 113 | // el.form.addEventListener("submit", (e) => { 114 | // Array.from((new FormData(el.form)).entries(), ([name, val]) => { 115 | // let input = el.form.querySelector(`[name="${name}"]`) 116 | // this.incCycle(input, DEBOUNCE_TRIGGER) 117 | // this.deletePrivate(input, THROTTLED) 118 | // }) 119 | // }) 120 | // } 121 | // if (this.once(el, "bind-debounce")) { 122 | // el.addEventListener("blur", (e) => this.triggerCycle(el, DEBOUNCE_TRIGGER)) 123 | // } 124 | // } 125 | // }, 126 | 127 | // triggerCycle(el, key, currentCycle) { 128 | // let [cycle, trigger] = this.private(el, key) 129 | // if (!currentCycle) { currentCycle = cycle } 130 | // if (currentCycle === cycle) { 131 | // this.incCycle(el, key) 132 | // trigger() 133 | // } 134 | // }, 135 | 136 | // once(el, key) { 137 | // if (this.private(el, key) === true) { return false } 138 | // this.putPrivate(el, key, true) 139 | // return true 140 | // }, 141 | 142 | // incCycle(el, key, trigger = function () { }) { 143 | // let [currentCycle, oldTrigger] = this.private(el, key) || [0, trigger] 144 | // currentCycle++ 145 | // this.putPrivate(el, key, [currentCycle, trigger]) 146 | // return currentCycle 147 | // }, 148 | 149 | // discardError(container, el, phxFeedbackFor) { 150 | // let field = el.getAttribute && el.getAttribute(phxFeedbackFor) 151 | // let input = field && container.querySelector(`#${field}`) 152 | // if (!input) { return } 153 | 154 | // if (!(this.private(input, PRESTO_HAS_FOCUSED) || this.private(input.form, PRESTO_HAS_SUBMITTED))) { 155 | // el.classList.add(PRESTO_NO_FEEDBACK_CLASS) 156 | // } 157 | // }, 158 | 159 | // isPhxChild(node) { 160 | // return node.getAttribute && node.getAttribute(PRESTO_PARENT_ID) 161 | // }, 162 | 163 | // dispatchEvent(target, eventString, detail = {}) { 164 | // let event = new CustomEvent(eventString, { bubbles: true, cancelable: true, detail: detail }) 165 | // target.dispatchEvent(event) 166 | // }, 167 | 168 | // cloneNode(node, html) { 169 | // if (typeof (html) === "undefined") { 170 | // return node.cloneNode(true) 171 | // } else { 172 | // let cloned = node.cloneNode(false) 173 | // cloned.innerHTML = html 174 | // return cloned 175 | // } 176 | // }, 177 | 178 | // mergeAttrs(target, source, exclude = []) { 179 | // let sourceAttrs = source.attributes 180 | // for (let i = sourceAttrs.length - 1; i >= 0; i--) { 181 | // let name = sourceAttrs[i].name 182 | // if (exclude.indexOf(name) < 0) { target.setAttribute(name, source.getAttribute(name)) } 183 | // } 184 | 185 | // let targetAttrs = target.attributes 186 | // for (let i = targetAttrs.length - 1; i >= 0; i--) { 187 | // let name = targetAttrs[i].name 188 | // if (!source.hasAttribute(name)) { target.removeAttribute(name) } 189 | // } 190 | // }, 191 | 192 | // mergeFocusedInput(target, source) { 193 | // // skip selects because FF will reset highlighted index for any setAttribute 194 | // if (!(target instanceof HTMLSelectElement)) { DOM.mergeAttrs(target, source, ["value"]) } 195 | // if (source.readOnly) { 196 | // target.setAttribute("readonly", true) 197 | // } else { 198 | // target.removeAttribute("readonly") 199 | // } 200 | // }, 201 | 202 | // restoreFocus(focused, selectionStart, selectionEnd) { 203 | // if (!DOM.isTextualInput(focused)) { return } 204 | // let wasFocused = focused.matches(":focus") 205 | // if (focused.readOnly) { focused.blur() } 206 | // if (!wasFocused) { focused.focus() } 207 | // if (focused.setSelectionRange && focused.type === "text" || focused.type === "textarea") { 208 | // focused.setSelectionRange(selectionStart, selectionEnd) 209 | // } 210 | // }, 211 | 212 | // isFormInput(el) { return /^(?:input|select|textarea)$/i.test(el.tagName) }, 213 | 214 | // syncAttrsToProps(el) { 215 | // if (el instanceof HTMLInputElement && CHECKABLE_INPUTS.indexOf(el.type.toLocaleLowerCase()) >= 0) { 216 | // el.checked = el.getAttribute("checked") !== null 217 | // } 218 | // }, 219 | 220 | // isTextualInput(el) { return FOCUSABLE_INPUTS.indexOf(el.type) >= 0 }, 221 | 222 | // isNowTriggerFormExternal(el, phxTriggerExternal) { 223 | // return el.getAttribute && el.getAttribute(phxTriggerExternal) !== null 224 | // }, 225 | 226 | // undoRefs(ref, container) { 227 | // DOM.all(container, `[${PRESTO_REF}]`, el => this.syncPendingRef(ref, el, el)) 228 | // }, 229 | 230 | // syncPendingRef(ref, fromEl, toEl) { 231 | // let fromRefAttr = fromEl.getAttribute && fromEl.getAttribute(PRESTO_REF) 232 | // if (fromRefAttr === null) { return true } 233 | 234 | // let fromRef = parseInt(fromRefAttr) 235 | // if (ref !== null && ref >= fromRef) { 236 | // [fromEl, toEl].forEach(el => { 237 | // // remove refs 238 | // el.removeAttribute(PRESTO_REF) 239 | // // retore inputs 240 | // if (el.getAttribute(PRESTO_READONLY) !== null) { 241 | // el.readOnly = false 242 | // el.removeAttribute(PRESTO_READONLY) 243 | // } 244 | // if (el.getAttribute(PRESTO_DISABLED) !== null) { 245 | // el.disabled = false 246 | // el.removeAttribute(PRESTO_DISABLED) 247 | // } 248 | // // remove classes 249 | // PRESTO_EVENT_CLASSES.forEach(className => DOM.removeClass(el, className)) 250 | // // restore disables 251 | // let disableRestore = el.getAttribute(PRESTO_DISABLE_WITH_RESTORE) 252 | // if (disableRestore !== null) { 253 | // el.innerText = disableRestore 254 | // el.removeAttribute(PRESTO_DISABLE_WITH_RESTORE) 255 | // } 256 | // }) 257 | // return true 258 | // } else { 259 | // PRESTO_EVENT_CLASSES.forEach(className => { 260 | // fromEl.classList.contains(className) && toEl.classList.add(className) 261 | // }) 262 | // toEl.setAttribute(PRESTO_REF, fromEl.getAttribute(PRESTO_REF)) 263 | // if (DOM.isFormInput(fromEl) || /submit/i.test(fromEl.type)) { 264 | // return false 265 | // } else { 266 | // return true 267 | // } 268 | // } 269 | // } 270 | } -------------------------------------------------------------------------------- /assets/js/presto.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* global document */ 4 | 5 | /** 6 | * Presto JavaScript client 7 | * 8 | * ## Setup 9 | * 10 | * Presto needs a Phoenix channel to communiacate on. It also needs a 11 | * reference to an Unpoly object for DOM manipulation. 12 | * 13 | * ```javascript 14 | * import "presto" 15 | * 16 | * let presto = new Presto(channel, up); 17 | * ``` 18 | * 19 | * @module presto 20 | */ 21 | 22 | import { Browser } from './browser'; 23 | export { Browser }; 24 | 25 | import { DOM } from './dom'; 26 | export { DOM }; 27 | 28 | import { Component } from './component'; 29 | export { Component }; 30 | 31 | import $ from 'cash-dom'; 32 | 33 | export class Presto { 34 | constructor() { 35 | this.callbacks = { 36 | onEvent: [], 37 | preUpdate: [], 38 | postUpdate: [] 39 | }; 40 | this.eventNamespace = '.presto' 41 | 42 | this.allEventNames = allEventNames().map((name) => { 43 | return name.replace(/^on/, ''); 44 | }); 45 | } 46 | bindEvents() { 47 | var self = this; 48 | 49 | // Attach a delegated event handler 50 | this.allEventNames.forEach((eventName) => { 51 | var prestoClass = '.presto-' + eventName; 52 | var namespacedName = eventName + this.eventNamespace; 53 | 54 | // events attached to internal elements 55 | $('body').on(namespacedName, prestoClass, (event) => { 56 | var prestoEvent = self.prepareEvent(namespacedName, event); 57 | self.runEventHooks(prestoEvent); 58 | }); 59 | 60 | // events attached to the body 61 | $('body' + prestoClass).on(namespacedName, (event) => { 62 | var prestoEvent = self.prepareEvent(namespacedName, event); 63 | self.runEventHooks(prestoEvent); 64 | }); 65 | }); 66 | } 67 | 68 | bindChannel(channel) { 69 | var self = this; 70 | 71 | self.onEvent(function (prestoEvent) { 72 | console.debug("[Presto] sending event", prestoEvent); 73 | channel.push("presto", prestoEvent); 74 | }); 75 | 76 | channel.on("presto", payload => { 77 | console.debug("[Presto] got event", payload); 78 | self.handleCommand(payload); 79 | }); 80 | } 81 | 82 | unbindEvents() { 83 | $('body').off(self.eventNamespace); 84 | } 85 | 86 | onEvent(callback) { this.callbacks.onEvent.push(callback) } 87 | onPreUpdate(callback) { this.callbacks.preUpdate.push(callback) } 88 | onPostUpdate(callback) { this.callbacks.postUpdate.push(callback) } 89 | 90 | runEventHooks(payload) { 91 | this.callbacks.onEvent.forEach(callback => callback(payload)) 92 | } 93 | 94 | runPreUpdateHooks(payload) { 95 | this.callbacks.preUpdate.forEach(callback => callback(payload)) 96 | } 97 | 98 | runPostUpdateHooks(payload) { 99 | this.callbacks.postUpdate.forEach(callback => callback(payload)) 100 | } 101 | 102 | handleCommand(payload) { 103 | var { name: name } = payload; 104 | switch (name) { 105 | case "update_component": { 106 | this.runPreUpdateHooks(payload); 107 | this.handleCommandUpdateComponent(payload); 108 | this.runPostUpdateHooks(payload); 109 | break; 110 | } 111 | default: 112 | this.handleCommandUnknown(payload); 113 | } 114 | } 115 | 116 | handleCommandUpdateComponent(payload) { 117 | var { component_id: componentId, content: content } = payload; 118 | Component.update(componentId, content); 119 | } 120 | 121 | handleCommandUnknown(payload) { 122 | console.warn("[Presto] Unable to handle payload: ", payload); 123 | } 124 | 125 | prepareEvent(name, event) { 126 | var $elem = $(event.target); 127 | 128 | // var $instance = $elem.parents(".presto-component-instance").toArray()[0]; 129 | // var $component = $elem.parents(".presto-component").toArray()[0]; 130 | var $instance = $elem.parents(".presto-component-instance"); 131 | var $component = $elem.parents(".presto-component"); 132 | 133 | var meta = name; 134 | if (event.keyCode) { 135 | meta = [name, event.keyCode] 136 | } 137 | 138 | var prestoEvent = { 139 | element: $elem.prop('tagName'), 140 | type: event.type, 141 | meta: meta, 142 | attrs: $elem.attr(), 143 | id: $elem.prop('id'), 144 | instance_id: $instance && $instance.attr('id'), 145 | component_id: $component && $component.attr('id') 146 | } 147 | 148 | return prestoEvent; 149 | } 150 | } 151 | 152 | // https://stackoverflow.com/questions/9368538/getting-an-array-of-all-dom-events-possible 153 | function allEventNames() { 154 | if (typeof jest !== 'undefined') { 155 | return ["onreadystatechange", "onpointerlockchange", "onpointerlockerror", "onbeforecopy", "onbeforecut", "onbeforepaste", "onfreeze", "onresume", "onsearch", "onsecuritypolicyviolation", "onvisibilitychange", "oncopy", "oncut", "onpaste", "onabort", "onblur", "oncancel", "oncanplay", "oncanplaythrough", "onchange", "onclick", "onclose", "oncontextmenu", "oncuechange", "ondblclick", "ondrag", "ondragend", "ondragenter", "ondragleave", "ondragover", "ondragstart", "ondrop", "ondurationchange", "onemptied", "onended", "onerror", "onfocus", "onformdata", "oninput", "oninvalid", "onkeydown", "onkeypress", "onkeyup", "onload", "onloadeddata", "onloadedmetadata", "onloadstart", "onmousedown", "onmouseenter", "onmouseleave", "onmousemove", "onmouseout", "onmouseover", "onmouseup", "onmousewheel", "onpause", "onplay", "onplaying", "onprogress", "onratechange", "onreset", "onresize", "onscroll", "onseeked", "onseeking", "onselect", "onstalled", "onsubmit", "onsuspend", "ontimeupdate", "ontoggle", "onvolumechange", "onwaiting", "onwebkitanimationend", "onwebkitanimationiteration", "onwebkitanimationstart", "onwebkittransitionend", "onwheel", "onauxclick", "ongotpointercapture", "onlostpointercapture", "onpointerdown", "onpointermove", "onpointerup", "onpointercancel", "onpointerover", "onpointerout", "onpointerenter", "onpointerleave", "onselectstart", "onselectionchange", "onanimationend", "onanimationiteration", "onanimationstart", "ontransitionend", "onfullscreenchange", "onfullscreenerror", "onwebkitfullscreenchange", "onwebkitfullscreenerror", "onpointerrawupdate"] 156 | } else { 157 | return Object.getOwnPropertyNames(document).concat(Object.getOwnPropertyNames(Object.getPrototypeOf(Object.getPrototypeOf(document)))).concat(Object.getOwnPropertyNames(Object.getPrototypeOf(window))).filter(function (i) { return !i.indexOf("on") && (document[i] == null || typeof document[i] == "function"); }).filter(function (elem, pos, self) { return self.indexOf(elem) == pos; }); 158 | } 159 | } 160 | 161 | // Extend jQuery with attr() 162 | (function (old) { 163 | $.fn.attr = function () { 164 | if (arguments.length === 0) { 165 | if (this.length === 0) { 166 | return null; 167 | } 168 | 169 | var obj = {}; 170 | $.each(this[0].attributes, function () { 171 | if (this.specified) { 172 | obj[this.name] = this.value; 173 | } 174 | }); 175 | return obj; 176 | } 177 | 178 | return old.apply(this, arguments); 179 | }; 180 | })($.fn.attr); -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "presto", 3 | "version": "0.1.1", 4 | "description": "The official JavaScript client for Presto.", 5 | "license": "Apache-2.0", 6 | "main": "./priv/static/presto.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/ijcd/presto.git" 10 | }, 11 | "author": "Ian Duggan ", 12 | "scripts": { 13 | "docs": "documentation build js/presto.js -f html -o ../doc/js", 14 | "build": "webpack --mode production", 15 | "watch": "webpack --mode development --watch", 16 | "test": "jest", 17 | "test.coverage": "jest --coverage", 18 | "test.watch": "jest --watch" 19 | }, 20 | "dependencies": { 21 | "cash-dom": "^7.0.3", 22 | "morphdom": "^2.6.1" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.3.4", 26 | "@babel/preset-env": "^7.4.1", 27 | "babel-loader": "^8.0.5", 28 | "expose-loader": "^0.7.5", 29 | "jest": "^26.0.1", 30 | "wait-for-expect": "^3.0.2", 31 | "webpack": "4.43.0", 32 | "webpack-cli": "^3.3.11" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /assets/tests/browser.test.js: -------------------------------------------------------------------------------- 1 | import { Browser } from '../js/presto.js' 2 | 3 | describe('Browser', () => { 4 | beforeEach(() => { 5 | clearCookies() 6 | }) 7 | 8 | describe('setCookie', () => { 9 | test('sets a cookie', () => { 10 | Browser.setCookie('apple', 1234) 11 | Browser.setCookie('orange', '5678') 12 | expect(document.cookie).toContain('apple') 13 | expect(document.cookie).toContain('1234') 14 | expect(document.cookie).toContain('orange') 15 | expect(document.cookie).toContain('5678') 16 | }) 17 | }) 18 | 19 | describe('getCookie', () => { 20 | test('returns the value for a cookie', () => { 21 | document.cookie = 'apple=1234' 22 | document.cookie = 'orange=5678' 23 | expect(Browser.getCookie('apple')).toEqual('1234') 24 | }) 25 | test('returns an empty string for a non-existent cookie', () => { 26 | document.cookie = 'apple=1234' 27 | document.cookie = 'orange=5678' 28 | expect(Browser.getCookie('plum')).toEqual('') 29 | }) 30 | }) 31 | 32 | describe('redirect', () => { 33 | const originalWindowLocation = global.window.location 34 | 35 | beforeEach(() => { 36 | delete global.window.location 37 | global.window.location = 'https://example.com' 38 | }) 39 | 40 | afterAll(() => { 41 | global.window.location = originalWindowLocation 42 | }) 43 | 44 | test('redirects to a new URL', () => { 45 | Browser.redirect('https://phoenixframework.com') 46 | expect(window.location).toEqual('https://phoenixframework.com') 47 | }) 48 | 49 | test('sets a flash cookie before redirecting', () => { 50 | Browser.redirect('https://phoenixframework.com', 'mango') 51 | expect(document.cookie).toContain('__phoenix_flash__') 52 | expect(document.cookie).toContain('mango') 53 | }) 54 | }) 55 | }) 56 | 57 | // Adapted from https://stackoverflow.com/questions/179355/clearing-all-cookies-with-javascript/179514#179514 58 | function clearCookies() { 59 | const cookies = document.cookie.split(';') 60 | 61 | for (let i = 0; i < cookies.length; i++) { 62 | const cookie = cookies[i] 63 | const eqPos = cookie.indexOf('=') 64 | const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie 65 | document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT' 66 | } 67 | } -------------------------------------------------------------------------------- /assets/tests/component.test.js: -------------------------------------------------------------------------------- 1 | import { Component } from "../js/presto" 2 | import { TestHelpers } from "./test_helpers" 3 | import $ from 'cash-dom'; 4 | 5 | describe('Component', () => { 6 | 7 | beforeEach(function () { 8 | TestHelpers.resetDocument(); 9 | }); 10 | 11 | describe('scan', () => { 12 | it('returns a Map', () => { 13 | var ps = Component.scan(); 14 | 15 | expect(ps).toBeInstanceOf(Map); 16 | expect(ps.size).toBe(0); 17 | }); 18 | 19 | it('finds a component', () => { 20 | TestHelpers.setRoot(` 21 |
22 |
23 | Counter is: 1 24 |
25 |
26 | `) 27 | var ps = Component.scan(); 28 | 29 | expect(ps.size).toBe(1); 30 | expect(Array.from(ps.keys())).toStrictEqual(['cA']); 31 | expect(ps.get('cA').has('iA')).toBe(true); 32 | }); 33 | 34 | it('finds several components', () => { 35 | TestHelpers.setRoot(` 36 |
37 |
38 | Counter is: 1 39 |
40 |
41 |
42 |
43 | Counter is: 1 44 |
45 |
46 |
47 |
48 | Counter is: 1 49 |
50 |
51 | `) 52 | var ps = Component.scan(); 53 | 54 | expect(ps.size).toBe(3); 55 | expect(Array.from(ps.keys())).toStrictEqual(['cA', 'cB', 'cC']); 56 | 57 | expect(ps.get('cA').has('iA')).toBe(true); 58 | expect(ps.get('cB').has('iB')).toBe(true); 59 | expect(ps.get('cC').has('iC')).toBe(true); 60 | }); 61 | 62 | it('finds two component instances for the same component-id', () => { 63 | TestHelpers.setRoot(` 64 |
65 |
66 | Counter is: 1 67 |
68 |
69 |
70 |
71 | Counter is: 1 72 |
73 |
74 |
75 |
76 | Counter is: 1 77 |
78 |
79 | `) 80 | var ps = Component.scan(); 81 | 82 | expect(ps.size).toBe(2); 83 | expect(Array.from(ps.keys())).toStrictEqual(['cA', 'cC']); 84 | 85 | expect(ps.get('cA').has('iA')).toBe(true); 86 | expect(ps.get('cA').has('iB')).toBe(true); 87 | expect(ps.get('cC').has('iC')).toBe(true); 88 | }); 89 | }); 90 | 91 | describe('update', () => { 92 | it('updates a component', () => { 93 | TestHelpers.setRoot(` 94 |
95 |
96 | Counter is: 1 97 |
98 |
99 | `) 100 | 101 | Component.update('cA', ` 102 |
103 | Counter is: 2 104 |
105 | `) 106 | 107 | expect($('div.presto-component-instance#iA .presto-component#cA').text().trim()).toEqual('Counter is: 2'); 108 | }); 109 | 110 | it('only updates component with matching component-id', () => { 111 | TestHelpers.setRoot(` 112 |
113 |
114 | Counter is: 1 115 |
116 |
117 |
118 |
119 | Counter is: 1 120 |
121 |
122 | `) 123 | 124 | Component.update('cA', ` 125 |
126 | Counter is: 2 127 |
128 | `) 129 | 130 | expect($('div.presto-component-instance#iA .presto-component#cA').text().trim()).toStrictEqual('Counter is: 2'); 131 | expect($('div.presto-component-instance#iB .presto-component#cB').text().trim()).toStrictEqual('Counter is: 1'); 132 | }); 133 | 134 | it('updates all components with same component-id', () => { 135 | TestHelpers.setRoot(` 136 |
137 |
138 | Counter is: 1 139 |
140 |
141 |
142 |
143 | Counter is: 1 144 |
145 |
146 | `) 147 | 148 | Component.update('cA', ` 149 |
150 | Counter is: 2 151 |
152 | `) 153 | 154 | expect($('div.presto-component-instance#iA .presto-component#cA').text().trim()).toStrictEqual('Counter is: 2'); 155 | expect($('div.presto-component-instance#iB .presto-component#cA').text().trim()).toStrictEqual('Counter is: 2'); 156 | }); 157 | 158 | 159 | it('preserves focus', () => { 160 | TestHelpers.setRoot(` 161 |
162 |
163 | Counter is: 1 164 |
165 |
166 | `) 167 | 168 | $('#cA')[0].focus() 169 | var startingActive = document.activeElement; 170 | 171 | Component.update('cA', ` 172 |
173 | Counter is: 2 174 |
175 | `) 176 | 177 | expect($('div.presto-component-instance#iA .presto-component#cA').text().trim()).toStrictEqual('Counter is: 2'); 178 | expect(document.activeElement).toEqual(startingActive); 179 | }); 180 | 181 | it('warns about update for missing component-id', () => { 182 | TestHelpers.setRoot(` 183 |
184 |
185 | Counter is: 1 186 |
187 |
188 | `); 189 | 190 | var warnings = TestHelpers.collectWarnings(() => { 191 | Component.update('doesNotExist', ` 192 |
193 | Counter is: 2 194 |
195 | `); 196 | }); 197 | 198 | expect(warnings).toStrictEqual(["[Presto] Ignoring request to update unknown componentId: doesNotExist"]); 199 | }); 200 | }); 201 | }); 202 | -------------------------------------------------------------------------------- /assets/tests/dom.test.js: -------------------------------------------------------------------------------- 1 | import { DOM } from "../js/presto.js" 2 | 3 | let appendTitle = opts => { 4 | let title = document.createElement("title") 5 | let { prefix, suffix } = opts 6 | if (prefix) { title.setAttribute("data-prefix", prefix) } 7 | if (suffix) { title.setAttribute("data-suffix", suffix) } 8 | document.head.appendChild(title) 9 | } 10 | 11 | let tag = (tagName, attrs, innerHTML) => { 12 | let el = document.createElement(tagName) 13 | el.innerHTML = innerHTML 14 | for (let key in attrs) { el.setAttribute(key, attrs[key]) } 15 | return el 16 | } 17 | 18 | describe("DOM", () => { 19 | beforeEach(() => { 20 | let curTitle = document.querySelector("title") 21 | curTitle && curTitle.remove() 22 | }) 23 | 24 | describe("putTitle", () => { 25 | test("with no attributes", () => { 26 | appendTitle({}) 27 | DOM.putTitle("My Title") 28 | expect(document.title).toBe("My Title") 29 | }) 30 | 31 | test("with prefix", () => { 32 | appendTitle({ prefix: "PRE " }) 33 | DOM.putTitle("My Title") 34 | expect(document.title).toBe("PRE My Title") 35 | }) 36 | 37 | test("with suffix", () => { 38 | appendTitle({ suffix: " POST" }) 39 | DOM.putTitle("My Title") 40 | expect(document.title).toBe("My Title POST") 41 | }) 42 | 43 | test("with prefix and suffix", () => { 44 | appendTitle({ prefix: "PRE ", suffix: " POST" }) 45 | DOM.putTitle("My Title") 46 | expect(document.title).toBe("PRE My Title POST") 47 | }) 48 | }) 49 | 50 | describe("findParentCIDs", () => { 51 | test("returns only parent cids", () => { 52 | let view = tag("div", {}, ` 53 |
60 |
61 | `) 62 | document.body.appendChild(view) 63 | 64 | expect(DOM.findParentCIDs(view, [1, 2, 3])).toEqual(new Set([1, 2, 3])) 65 | 66 | view.appendChild(tag("div", { "data-presto-component": 1 }, ` 67 |
68 | `)) 69 | expect(DOM.findParentCIDs(view, [1, 2, 3])).toEqual(new Set([1, 3])) 70 | 71 | view.appendChild(tag("div", { "data-presto-component": 1 }, ` 72 |
73 |
74 |
75 | `)) 76 | expect(DOM.findParentCIDs(view, [1, 2, 3])).toEqual(new Set([1])) 77 | }) 78 | }) 79 | 80 | describe("findFirstComponentNode", () => { 81 | test("returns the first node with cid ID", () => { 82 | let component = tag("div", { "data-presto-component": 0 }, ` 83 |
84 | `) 85 | document.body.appendChild(component) 86 | 87 | expect(DOM.findFirstComponentNode(document, 0)).toBe(component) 88 | }) 89 | 90 | test("returns null with no matching cid", () => { 91 | expect(DOM.findFirstComponentNode(document, 123)).toBe(null) 92 | }) 93 | }) 94 | 95 | // test("isNowTriggerFormExternal", () => { 96 | // let form 97 | // form = tag("form", { "phx-trigger-external": "" }, "") 98 | // expect(DOM.isNowTriggerFormExternal(form, "phx-trigger-external")).toBe(true) 99 | 100 | // form = tag("form", {}, "") 101 | // expect(DOM.isNowTriggerFormExternal(form, "phx-trigger-external")).toBe(false) 102 | // }) 103 | 104 | // test("undoRefs restores phx specific attributes awaiting a ref", () => { 105 | // let content = ` 106 | // 107 | //
108 | // 109 | // 110 | // 111 | // 112 | //
113 | // `.trim() 114 | // let div = tag("div", {}, content) 115 | 116 | // DOM.undoRefs(1, div) 117 | // expect(div.innerHTML).toBe(` 118 | // 119 | //
120 | // 121 | // 122 | // 123 | // 124 | //
125 | // `.trim()) 126 | 127 | // DOM.undoRefs(38, div) 128 | // expect(div.innerHTML).toBe(` 129 | // 130 | //
131 | // 132 | // 133 | // 134 | // 135 | //
136 | // `.trim()) 137 | // }) 138 | }) -------------------------------------------------------------------------------- /assets/tests/presto.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import $ from "cash-dom"; 4 | import { Presto } from '../js/presto'; 5 | import { TestHelpers } from './test_helpers' 6 | 7 | describe('PrestoLib', () => { 8 | 9 | beforeEach(function () { 10 | TestHelpers.resetDocument(); 11 | }); 12 | 13 | //////////// 14 | // PRESTO // 15 | //////////// 16 | 17 | describe('Presto', () => { 18 | 19 | describe('constructor', () => { 20 | it('constructs', () => { 21 | var presto = new Presto(); 22 | expect(presto).toBeTruthy(); 23 | }); 24 | }); 25 | 26 | describe('bindEvents', () => { 27 | it('binds events at the component level', () => { 28 | TestHelpers.setRoot(` 29 |
30 |
31 | Counter is: 1 32 | 33 |
34 |
35 | `) 36 | var presto = new Presto() 37 | 38 | var fired = false; 39 | presto.bindEvents() 40 | presto.onEvent(function (prestoEvent) { 41 | // fired = prestoEvent.type; // cash-dom is having issues, using meta instead 42 | fired = prestoEvent.meta; 43 | }) 44 | 45 | $('button#theButton').trigger('click') 46 | expect(fired).toBe('click.presto'); 47 | fired = false; 48 | 49 | $('button#theButton').trigger('mouseenter') 50 | expect(fired).toBe('mouseenter.presto'); 51 | fired = false; 52 | 53 | $('button#theButton').trigger('mousedown') 54 | expect(fired).toBe('mousedown.presto'); 55 | fired = false; 56 | }); 57 | 58 | it('binds events at the body level', () => { 59 | $('body').addClass('presto-click') 60 | $('body').addClass('presto-mouseenter') 61 | $('body').addClass('presto-mousedown') 62 | var presto = new Presto() 63 | 64 | var fired = false; 65 | presto.bindEvents() 66 | presto.onEvent(function (prestoEvent) { 67 | // fired = prestoEvent.type; // cash-dom is having issues, using meta instead 68 | fired = prestoEvent.meta; 69 | }) 70 | 71 | $('#root').trigger('click') 72 | expect(fired).toBe('click.presto'); 73 | fired = false; 74 | 75 | $('#root').trigger('mouseenter') 76 | expect(fired).toBe('mouseenter.presto'); 77 | fired = false; 78 | 79 | $('#root').trigger('mousedown') 80 | expect(fired).toBe('mousedown.presto'); 81 | fired = false; 82 | }); 83 | }); 84 | 85 | describe('unbindEvents', () => { 86 | it('unbinds all events in namespace', () => { 87 | TestHelpers.setRoot(` 88 |
89 |
90 | Counter is: 1 91 | 92 |
93 |
94 | `) 95 | var presto = new Presto() 96 | 97 | var fired = false; 98 | presto.bindEvents(); 99 | presto.onEvent(function (prestoEvent) { 100 | // fired = prestoEvent.type; // cash-dom is having issues, using meta instead 101 | fired = prestoEvent.meta; 102 | }) 103 | 104 | // verify working 105 | $('button#theButton').trigger('click') 106 | expect(fired).toBe('click.presto'); 107 | fired = false; 108 | 109 | $('#root').trigger('click') 110 | expect(fired).toBe('click.presto'); 111 | fired = false; 112 | 113 | // unbind 114 | presto.unbindEvents(); 115 | 116 | // verify unbound 117 | $('button#theButton').trigger('click') 118 | expect(fired).toBe(false); 119 | 120 | $('button#theButton').trigger('mouseenter') 121 | expect(fired).toBe(false); 122 | 123 | $('button#theButton').trigger('mousedown') 124 | expect(fired).toBe(false); 125 | 126 | $('#root').trigger('click') 127 | expect(fired).toBe(false); 128 | 129 | $('#root').trigger('mouseenter') 130 | expect(fired).toBe(false); 131 | 132 | $('#root').trigger('mousedown') 133 | expect(fired).toBe(false); 134 | }); 135 | }); 136 | 137 | describe('handleCommand', () => { 138 | describe('unknown command', () => { 139 | 140 | it('warns by default', () => { 141 | var presto = new Presto() 142 | var fooload = { name: "foo" }; 143 | 144 | var warnings = TestHelpers.collectWarnings(() => { 145 | presto.handleCommand(fooload); 146 | }); 147 | 148 | expect(warnings).toStrictEqual(["[Presto] Unable to handle payload: ", fooload]); 149 | }); 150 | 151 | it('calls a custom function', () => { 152 | var presto = new Presto() 153 | var fooload = { name: "foo" }; 154 | 155 | var called = false; 156 | var calledWith = null; 157 | presto.handleCommandUnknown = (payload) => { 158 | called = true; 159 | calledWith = payload; 160 | }; 161 | 162 | presto.handleCommand(fooload); 163 | 164 | expect(called).toBe(true); 165 | expect(calledWith).toBe(fooload); 166 | }); 167 | }); 168 | 169 | describe('update_component', () => { 170 | it('updates a component', () => { 171 | TestHelpers.setRoot(` 172 |
173 |
174 | Counter is: 1 175 |
176 |
177 | `) 178 | var presto = new Presto(); 179 | 180 | presto.handleCommand({ 181 | name: "update_component", 182 | component_id: "cA", 183 | content: `
Counter is: 2
` 184 | }); 185 | 186 | expect( 187 | $('div.presto-component-instance#iA .presto-component#cA').text().trim() 188 | ).toEqual('Counter is: 2'); 189 | }); 190 | 191 | it('calls pre/post update hooks', () => { 192 | TestHelpers.setRoot(` 193 |
194 |
195 | Counter is: 1 196 |
197 |
198 | `) 199 | var calls = []; 200 | var presto = new Presto(); 201 | 202 | presto.onPreUpdate(() => { 203 | calls.push("pre 1"); 204 | }); 205 | presto.onPreUpdate(() => { 206 | calls.push("pre 2"); 207 | }); 208 | presto.onPostUpdate(() => { 209 | calls.push("post 1"); 210 | }); 211 | presto.onPostUpdate(() => { 212 | calls.push("post 2"); 213 | }); 214 | 215 | presto.handleCommand({ 216 | name: "update_component", 217 | component_id: "cA", 218 | content: `
Counter is: 2
` 219 | }); 220 | 221 | expect(calls).toStrictEqual(["pre 1", "pre 2", "post 1", "post 2"]); 222 | }); 223 | }); 224 | }); 225 | 226 | describe('onEvent', () => { 227 | it('runs callback with annotated DOM event', () => { 228 | TestHelpers.setRoot(` 229 |
230 |
231 | Counter is: 1 232 | 233 |
234 |
235 | `) 236 | var presto = new Presto() 237 | 238 | var theEvent = null; 239 | debugger; 240 | presto.bindEvents() 241 | presto.onEvent(function (prestoEvent) { 242 | theEvent = prestoEvent; 243 | }) 244 | 245 | $('button#theButton').trigger('click') 246 | expect(theEvent.element).toEqual("BUTTON"); 247 | expect(theEvent.type).toEqual("click"); 248 | expect(theEvent.attrs).toStrictEqual({ id: "theButton", class: "presto-click presto-mouseenter presto-mousedown" }); 249 | expect(theEvent.id).toEqual("theButton"); 250 | expect(theEvent.instance_id).toEqual("iA"); 251 | expect(theEvent.component_id).toEqual("cA"); 252 | }); 253 | 254 | it('handles nested components properly', () => { 255 | TestHelpers.setRoot(` 256 |
257 |
258 |
259 |
260 | Counter is: 1 261 | 262 |
263 |
264 |
265 |
266 | `) 267 | var presto = new Presto() 268 | 269 | var theEvent = null; 270 | presto.bindEvents() 271 | presto.onEvent(function (prestoEvent) { 272 | theEvent = prestoEvent; 273 | }) 274 | 275 | $('button#theButton').trigger('click') 276 | expect(theEvent.element).toEqual("BUTTON"); 277 | expect(theEvent.type).toEqual("click"); 278 | expect(theEvent.attrs).toStrictEqual({ id: "theButton", class: "presto-click presto-mouseenter presto-mousedown" }); 279 | expect(theEvent.id).toEqual("theButton"); 280 | expect(theEvent.instance_id).toEqual("iA"); 281 | expect(theEvent.component_id).toEqual("cA"); 282 | }); 283 | }); 284 | }); 285 | }); 286 | -------------------------------------------------------------------------------- /assets/tests/test_helpers.js: -------------------------------------------------------------------------------- 1 | export let TestHelpers = { 2 | 3 | resetDocument() { 4 | return document.body.innerHTML = `
`; 5 | }, 6 | 7 | setRoot(what) { 8 | return document.querySelector('#root').innerHTML = what; 9 | }, 10 | 11 | collectWarnings(f) { 12 | var oldWarn = console.warn; 13 | var warnings = []; 14 | 15 | try { 16 | console.warn = (...s) => { 17 | warnings.push(...s); 18 | // oldWarn(...s) 19 | }; 20 | f(); 21 | } finally { 22 | console.warn = oldWarn; 23 | } 24 | 25 | return warnings; 26 | } 27 | } -------------------------------------------------------------------------------- /assets/webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path') 3 | 4 | module.exports = { 5 | entry: './js/presto.js', 6 | output: { 7 | filename: 'presto.js', 8 | path: path.resolve(__dirname, '../priv/static'), 9 | library: 'Presto', 10 | libraryTarget: 'umd', 11 | globalObject: 'this' 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: path.resolve(__dirname, './js/presto.js'), 17 | use: [{ 18 | loader: 'expose-loader', 19 | options: 'Presto' 20 | }] 21 | }, 22 | { 23 | test: /\.js$/, 24 | exclude: /node_modules/, 25 | use: { 26 | loader: 'babel-loader' 27 | } 28 | } 29 | ] 30 | }, 31 | plugins: [] 32 | } -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :presto, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:presto, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | 32 | if Mix.env == :dev do 33 | config :mix_test_watch, 34 | tasks: [ 35 | "test", 36 | # "dialyzer", 37 | # "credo", 38 | ] 39 | end 40 | -------------------------------------------------------------------------------- /lib/presto.ex: -------------------------------------------------------------------------------- 1 | defmodule Presto do 2 | @moduledoc """ 3 | Presto pages! 4 | """ 5 | 6 | @type component_id() :: Registry.key() 7 | @type component_module() :: atom() 8 | @type component_create() :: {:ok, Presto.component_id()} | {:error, term()} 9 | @type component_event() :: term() 10 | @type component_ref() :: {:presto_component, pid()} 11 | @type component_instance :: {:safe, term()} 12 | 13 | @doc """ 14 | Creates a new component process based on the `component_module`, and `component_key`. 15 | Returns a tuple such as `{:ok, component_key}` if successful. If there is an 16 | issue, an `{:error, reason}` tuple is returned. 17 | """ 18 | @spec create_component(component_module(), component_id()) :: component_create() 19 | def create_component(component_module, component_id) do 20 | case Presto.ComponentSupervisor.start_component(component_module, component_id) do 21 | {:ok, pid} -> {:ok, pid} 22 | {:error, {:already_started, _pid}} -> {:error, :process_already_exists} 23 | other -> {:error, other} 24 | end 25 | end 26 | 27 | @doc """ 28 | Finds an existing component process.. 29 | Returns a tuple such as `{:ok, component_id}` if successful. If there is an 30 | issue, an `{:error, reason}` tuple is returned. 31 | """ 32 | @spec find_component(component_id()) :: component_create() 33 | def find_component(component_id) do 34 | case Registry.lookup(Presto.ComponentRegistry, component_id) do 35 | [] -> {:error, :no_such_component} 36 | [{pid, _meta}] -> {:ok, pid} 37 | end 38 | end 39 | 40 | @doc """ 41 | Finds an existing component process based on the `component_module`, and `component_key`. 42 | If the component is not found, it is created and returned instead. 43 | Returns a tuple such as `{:ok, component_key}` if successful. If there is an 44 | issue, an `{:error, reason}` tuple is returned. 45 | """ 46 | @spec find_or_create_component(component_module(), component_id()) :: component_create() 47 | def find_or_create_component(component_module, component_id) do 48 | case Registry.lookup(Presto.ComponentRegistry, component_id) do 49 | [] -> 50 | create_component(component_module, component_id) 51 | 52 | [{pid, _meta}] -> 53 | {:ok, pid} 54 | end 55 | end 56 | 57 | @doc """ 58 | Determines if a `Presto.Component` process exists, based on the `component_key` 59 | provided. Returns a boolean. 60 | 61 | ## Example 62 | 63 | iex> Presto.component_exists?(DemoComponent, 6) 64 | false 65 | 66 | """ 67 | @spec component_exists?(component_id) :: boolean 68 | def component_exists?(component_id) do 69 | case find_component(component_id) do 70 | {:error, :no_such_component} -> false 71 | {:ok, pid} when is_pid(pid) -> true 72 | end 73 | end 74 | 75 | @doc """ 76 | Send an event to a component. 77 | """ 78 | @spec dispatch(component_event) :: any 79 | def dispatch(%{"component_id" => component_id} = message) do 80 | {:ok, pid} = Presto.find_component(component_id) 81 | Presto.Component.update(pid, message) 82 | end 83 | 84 | @doc """ 85 | Embed a component. 86 | """ 87 | # @spec component(component_module) :: any 88 | # def component(component_module) do 89 | # component_id = 90 | # component(component_module, component_id) 91 | # end 92 | 93 | @spec component(component_module, component_id) :: any 94 | def component(component_module, component_id \\ nil) do 95 | component_id = component_id || :crypto.strong_rand_bytes(16) 96 | encoded_id = encode_id(component_id) 97 | {:ok, pid} = find_or_create_component(component_module, encoded_id) 98 | {:presto_component, pid} 99 | end 100 | 101 | @spec render(component_ref) :: component_instance() 102 | def render({:presto_component, pid}) do 103 | {:ok, component} = Presto.Component.render(pid) 104 | instance = Phoenix.HTML.Tag.content_tag(:div, component, class: "presto-component-instance", id: make_instance_id()) 105 | instance 106 | end 107 | 108 | def render_component(component, component_id \\ nil) do 109 | cb = Application.get_env(:presto, :component_base) 110 | Module.concat(cb, component) 111 | |> Presto.component(component_id) 112 | |> Presto.render() 113 | end 114 | 115 | # component_ids need to be stable 116 | def encode_id(id) do 117 | key = Application.get_env(:presto, :secret_key_base) 118 | data = :erlang.term_to_binary(id) 119 | :crypto.hmac(:sha256, key, data) |> Base.encode16() 120 | end 121 | 122 | # instance_ids should be unique and unguessable 123 | defp make_instance_id() do 124 | :crypto.strong_rand_bytes(32) |> Base.encode16() 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/presto/action.ex: -------------------------------------------------------------------------------- 1 | defmodule Presto.Action do 2 | @type t :: UpdateComponent.t() | Custom.t() 3 | 4 | defmodule UpdateComponent do 5 | defstruct name: "update_component", 6 | component_id: nil, 7 | content: nil 8 | 9 | @type t :: %__MODULE__{ 10 | name: String.t(), 11 | component_id: String.t(), 12 | content: String.t() 13 | } 14 | end 15 | 16 | defmodule Custom do 17 | defstruct name: "custom", 18 | component_id: nil, 19 | content: nil 20 | 21 | @type t :: %__MODULE__{ 22 | name: String.t(), 23 | component_id: String.t(), 24 | content: String.t() 25 | } 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/presto/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Presto.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | def start(_type, _args) do 9 | # List all child processes to be supervised 10 | children = [ 11 | {Registry, keys: :unique, name: Presto.ComponentRegistry}, 12 | {Presto.ComponentSupervisor, []} 13 | ] 14 | 15 | # See https://hexdocs.pm/elixir/Supervisor.html 16 | # for other strategies and supported options 17 | opts = [strategy: :one_for_one, name: Presto.Supervisor] 18 | Supervisor.start_link(children, opts) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/presto/channel.ex: -------------------------------------------------------------------------------- 1 | defmodule Presto.Channel do 2 | 3 | # # Plug.Conn callbacks 4 | # @callback init(Plug.opts()) :: Plug.opts() 5 | # @callback call(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t() 6 | 7 | # # Component addressing 8 | # @callback component_id(assigns) :: term() 9 | # @callback key_spec(Presto.component_key()) :: term() 10 | 11 | # # State, update, and render 12 | # @callback initial_model(model()) :: term() 13 | # @callback update(message(), model()) :: model() 14 | # @callback render(model()) :: Util.safe() 15 | 16 | defmacro __using__(_opts) do 17 | quote location: :keep do 18 | @behaviour Presto.Channel 19 | 20 | def join("presto:lobby", payload, socket) do 21 | # %{visitor_id: visitor_id} = socket.assigns 22 | 23 | if authorized?(payload) do 24 | {:ok, socket} 25 | else 26 | {:error, %{reason: "unauthorized"}} 27 | end 28 | end 29 | 30 | def handle_in("presto", payload, socket) do 31 | # %{visitor_id: visitor_id} = socket.assigns 32 | 33 | # send event to presto page 34 | {:ok, dispatch} = Presto.dispatch(payload) 35 | 36 | case dispatch do 37 | [] -> nil 38 | _ -> push(socket, "presto", dispatch) 39 | end 40 | 41 | {:reply, {:ok, payload}, socket} 42 | end 43 | 44 | # Add authorization logic here as required. 45 | defp authorized?(_payload) do 46 | true 47 | end 48 | 49 | # defoverridable Presto.Channel 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/presto/component.ex: -------------------------------------------------------------------------------- 1 | defmodule Presto.Component do 2 | use GenServer, restart: :transient 3 | alias Presto.Util 4 | 5 | @type message :: term() 6 | @type model :: term() 7 | @type assigns :: Keyword.t() | map 8 | 9 | # Plug.Conn callbacks 10 | @callback init(Plug.opts()) :: Plug.opts() 11 | @callback call(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t() 12 | 13 | # State, update, and render 14 | @callback initial_model(model()) :: term() 15 | @callback update(message(), model()) :: model() 16 | @callback render(model()) :: Util.safe() 17 | 18 | defmacro __using__(_opts) do 19 | quote location: :keep do 20 | @behaviour Presto.Component 21 | 22 | def init([]), do: :index 23 | 24 | def call(conn, :index) do 25 | assigns = Map.put(conn.assigns, :conn, conn) 26 | 27 | {:safe, body} = Presto.component(__MODULE__) 28 | 29 | conn 30 | |> Plug.Conn.put_resp_header("content-type", "text/html; charset=utf-8") 31 | |> Plug.Conn.send_resp(200, body) 32 | end 33 | 34 | def update(_message, model), do: model 35 | 36 | def render(model), do: {:safe, inspect(model)} 37 | 38 | def initial_model(model), do: model 39 | 40 | defoverridable Presto.Component 41 | end 42 | end 43 | 44 | ###################### 45 | ### Client Methods ### 46 | ###################### 47 | 48 | defmodule State do 49 | defstruct component_module: nil, 50 | component_id: nil, 51 | model: %{} 52 | end 53 | 54 | @doc """ 55 | Starts a `Presto.Component` GenServer 56 | """ 57 | def start_link(component_module, component_id, initial_model \\ %{}) do 58 | name = via_tuple(component_id) 59 | model = component_module.initial_model(initial_model) 60 | 61 | initial_state = %State{component_id: component_id, component_module: component_module, model: model} 62 | 63 | GenServer.start_link(__MODULE__, initial_state, name: name) 64 | end 65 | 66 | @doc """ 67 | Sends an update message to the Component, returning the newly 68 | rendered content. 69 | """ 70 | def update(component, message) do 71 | GenServer.call(component, {:update, message}) 72 | end 73 | 74 | @doc """ 75 | Sends an update message to the Component, returning the newly 76 | rendered content. 77 | """ 78 | def render(component) do 79 | GenServer.call(component, :render) 80 | end 81 | 82 | ###################### 83 | ### Server Methods ### 84 | ###################### 85 | 86 | @doc """ 87 | Initializes state with the component_module and initial model from 88 | `start_link` 89 | """ 90 | def init(initial_state) do 91 | {:ok, initial_state} 92 | end 93 | 94 | @doc """ 95 | Renders the current state by calling `render(model)` with the 96 | current model state. 97 | """ 98 | def handle_call(:render, _from, state) do 99 | reply = {:ok, do_render(state)} 100 | {:reply, reply, state} 101 | end 102 | 103 | @doc """ 104 | Performs an update operation by calling `update(message, model)` 105 | on the component_module module from `init` 106 | """ 107 | def handle_call({:update, message}, _from, state) do 108 | new_state = do_update(message, state) 109 | 110 | content = 111 | new_state 112 | |> do_render() 113 | |> Util.safe_to_string() 114 | 115 | reply = 116 | {:ok, 117 | %Presto.Action.UpdateComponent{ 118 | component_id: state.component_id, 119 | content: content 120 | }} 121 | 122 | {:reply, reply, new_state} 123 | end 124 | 125 | ###################### 126 | ### Helper Methods ### 127 | ###################### 128 | 129 | defp do_update(message, state = %{model: model, component_module: component_module}) do 130 | new_model = component_module.update(message, model) 131 | %{state | model: new_model} 132 | end 133 | 134 | defp do_render(%{model: model, component_module: component_module, component_id: component_id}) do 135 | content = component_module.render(model) 136 | Phoenix.HTML.Tag.content_tag(:div, content, class: "presto-component", id: component_id) 137 | end 138 | 139 | defp via_tuple(component_id) do 140 | {:via, Registry, {Presto.ComponentRegistry, component_id}} 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /lib/presto/component_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Presto.ComponentSupervisor do 2 | @moduledoc """ 3 | DynamicSupervisor to handle the creation of dynamic 4 | `Presto.Component` processes. 5 | 6 | These processes will spawn for each `component_id` provided to the 7 | `Presto.Component.start_link` function. 8 | 9 | Functions contained in this supervisor module will assist in the 10 | creation and retrieval of new component processes. 11 | """ 12 | 13 | use DynamicSupervisor 14 | require Logger 15 | 16 | ###################### 17 | ### Client Methods ### 18 | ###################### 19 | 20 | @doc """ 21 | Starts the supervisor. 22 | """ 23 | def start_link(_args \\ []) do 24 | DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__) 25 | end 26 | 27 | @doc """ 28 | Starts a supervised Presto.Component process. 29 | """ 30 | @spec start_component(Presto.component_module(), Presto.component_id(), Presto.Component.model()) :: term() 31 | def start_component(component_module, component_id, initial_model \\ %{}) do 32 | spec = %{ 33 | id: Presto.Component, 34 | start: {Presto.Component, :start_link, [component_module, component_id, initial_model]}, 35 | restart: :transient 36 | } 37 | 38 | DynamicSupervisor.start_child(__MODULE__, spec) 39 | end 40 | 41 | ###################### 42 | ### Server Methods ### 43 | ###################### 44 | 45 | def init(initial_args) do 46 | DynamicSupervisor.init( 47 | strategy: :one_for_one, 48 | extra_arguments: initial_args 49 | ) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/presto/util.ex: -------------------------------------------------------------------------------- 1 | defmodule Presto.Util do 2 | @typedoc "Guaranteed to be safe" 3 | @type safe :: {:safe, iodata} 4 | 5 | @doc """ 6 | Fails if the result is not safe. In such cases, you can 7 | invoke `html_escape/1` or `raw/1`. 8 | """ 9 | @spec safe_to_string(safe) :: String.t() 10 | def safe_to_string({:safe, iodata}) do 11 | IO.iodata_to_binary(iodata) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Presto.Mixfile do 2 | use Mix.Project 3 | 4 | @name "Presto" 5 | @version "0.1.2" 6 | @maintainers ["Ian Duggan"] 7 | @licenses ["Apache 2.0"] 8 | @source_url "https://github.com/ijcd/presto" 9 | @description "Server-side single page apps in Elixir." 10 | 11 | def project do 12 | [ 13 | app: :presto, 14 | version: @version, 15 | elixir: "~> 1.5", 16 | elixirc_paths: elixirc_paths(Mix.env()), 17 | start_permanent: Mix.env() == :prod, 18 | aliases: aliases(), 19 | deps: deps(), 20 | 21 | # docs 22 | description: @description, 23 | name: @name, 24 | source_url: @source_url, 25 | package: package(), 26 | dialyzer: [flags: "--fullpath"], 27 | docs: [ 28 | main: "readme", 29 | source_ref: "v#{@version}", 30 | source_url: @source_url, 31 | extras: [ 32 | "README.md" 33 | ] 34 | ] 35 | ] 36 | end 37 | 38 | defp elixirc_paths(:test), do: ["lib", "test/support"] 39 | defp elixirc_paths(_), do: ["lib"] 40 | 41 | # Run "mix help compile.app" to learn about applications. 42 | def application do 43 | [ 44 | extra_applications: [:logger], 45 | mod: {Presto.Application, []} 46 | ] 47 | end 48 | 49 | # Run "mix help deps" to learn about dependencies. 50 | defp deps do 51 | [ 52 | # docs 53 | {:ex_doc, "~> 0.16.4", only: :dev, runtime: false}, 54 | {:earmark, "~> 1.2", only: :dev, runtime: false}, 55 | {:phoenix_html, "~> 2.10"}, 56 | # {:taggart, "~> 0.1"}, 57 | 58 | # dev/test 59 | {:mix_test_watch, "~> 0.3", only: [:dev, :test], runtime: false}, 60 | {:credo, "~> 0.8", only: [:dev, :test], runtime: false}, 61 | {:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false} 62 | ] 63 | end 64 | 65 | defp package do 66 | [ 67 | description: @description, 68 | files: ~w(lib priv config mix.exs README* LICENSE.md package.json), 69 | maintainers: @maintainers, 70 | licenses: @licenses, 71 | links: %{GitHub: @source_url} 72 | ] 73 | end 74 | 75 | # Aliases are shortcuts or tasks specific to the current project. 76 | # For example, to create, migrate and run the seeds file at once: 77 | # 78 | # $ mix ecto.setup 79 | # 80 | # See the documentation for `Mix` for more info on aliases. 81 | defp aliases do 82 | [ 83 | test: ["test --no-start"] 84 | ] 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, 3 | "credo": {:hex, :credo, "0.9.0", "5d1b494e4f2dc672b8318e027bd833dda69be71eaac6eedd994678be74ef7cb4", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "61de62970f70111434b84ec272cc969ac693af1f3e112f23f1ef055e57e838e1"}, 4 | "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm", "6c32a70ed5d452c6650916555b1f96c79af5fc4bf286997f8b15f213de786f73"}, 5 | "earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], [], "hexpm", "c57508ddad47dfb8038ca6de1e616e66e9b87313220ac5d9817bc4a4dc2257b9"}, 6 | "ex_doc": {:hex, :ex_doc, "0.16.4", "4bf6b82d4f0a643b500366ed7134896e8cccdbab4d1a7a35524951b25b1ec9f0", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm", "90e39c592b4952d35eb122b70d656a8b2ffa248d7b0fa428b314ce2176c69861"}, 7 | "file_system": {:hex, :file_system, "0.2.4", "f0bdda195c0e46e987333e986452ec523aed21d784189144f647c43eaf307064", [:mix], [], "hexpm", "d3d5ee3a1d656cb1efa0d0446df2aeb230c55d0d5fa2ab2840082f7ace50d04a"}, 8 | "floki": {:hex, :floki, "0.20.1", "4ee125a81605f5920189ac6afbee8dbf4de7808319e726f0d57781e660185980", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}, {:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"}, 9 | "html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm"}, 10 | "mime": {:hex, :mime, "1.2.0", "78adaa84832b3680de06f88f0997e3ead3b451a440d183d688085be2d709b534", [:mix], [], "hexpm", "b24d1d704209b760aeac161255c8031c5160de1a5cb7dd28bb84ef5bda2ba29e"}, 11 | "mix_test_watch": {:hex, :mix_test_watch, "0.6.0", "5e206ed04860555a455de2983937efd3ce79f42bd8536fc6b900cc286f5bb830", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "ea6f2a3766f18c2f53ca5b2d40b623ce2831c1646f36ff2b608607e20fc6c63c"}, 12 | "mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [:rebar3], [], "hexpm"}, 13 | "phoenix_html": {:hex, :phoenix_html, "2.11.1", "77b6f7fbd252168c6ec4f573de648d37cc5258cda13266ef001fbf99267eb6f3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f54c45774f0819b495bd4ab25a72429278b0f6de3410f9cb6993c5a267cfaee6"}, 14 | "plug": {:hex, :plug, "1.5.0", "224b25b4039bedc1eac149fb52ed456770b9678bbf0349cdd810460e1e09195b", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "cd9cd75c9c638c7e3ac9bfb0501d746fcfcf76cdb2d7b181ce93d4a02e52fcf2"}, 15 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, 16 | "taggart": {:hex, :taggart, "0.1.4", "96ffa096137e873680a062add620ddca6a7a1d363a2563c64c90f3093372fd86", [:mix], [{:floki, "~> 0.17", [hex: :floki, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.10", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm"}, 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "presto", 3 | "version": "0.1.2", 4 | "description": "The official JavaScript client for Presto.", 5 | "license": "Apache-2.0", 6 | "main": "./priv/static/presto.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/ijcd/presto.git" 10 | }, 11 | "author": "Ian Duggan ", 12 | "files": [ 13 | "README.md", 14 | "LICENSE.md", 15 | "package.json", 16 | "priv/static/presto.js", 17 | "assets/js/presto.js" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /priv/static/prestoTest.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Presto=t():e.Presto=t()}(window,function(){return function(e){var t={};function n(o){if(t[o])return t[o].exports;var r=t[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,n),r.l=!0,r.exports}return n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:o})},n.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=40)}([function(e,t){ 2 | /*! 3 | * Chai - flag utility 4 | * Copyright(c) 2012-2014 Jake Luer 5 | * MIT Licensed 6 | */ 7 | e.exports=function(e,t,n){var o=e.__flags||(e.__flags=Object.create(null));if(3!==arguments.length)return o[t];o[t]=n}},function(e,t){ 8 | /*! 9 | * Chai - transferFlags utility 10 | * Copyright(c) 2012-2014 Jake Luer 11 | * MIT Licensed 12 | */ 13 | e.exports=function(e,t,n){var o=e.__flags||(e.__flags=Object.create(null));for(var r in t.__flags||(t.__flags=Object.create(null)),n=3!==arguments.length||n,o)(n||"object"!==r&&"ssfi"!==r&&"lockSsfi"!==r&&"message"!=r)&&(t.__flags[r]=o[r])}},function(e,t,n){ 14 | /*! 15 | * chai 16 | * Copyright(c) 2011-2014 Jake Luer 17 | * MIT Licensed 18 | */ 19 | var o=[]; 20 | /*! 21 | * Chai version 22 | */t.version="4.2.0", 23 | /*! 24 | * Assertion Error 25 | */ 26 | t.AssertionError=n(14); 27 | /*! 28 | * Utils for plugins (not exported) 29 | */ 30 | var r=n(38);t.use=function(e){return~o.indexOf(e)||(e(t,r),o.push(e)),t}, 31 | /*! 32 | * Utility Functions 33 | */ 34 | t.util=r; 35 | /*! 36 | * Configuration 37 | */ 38 | var i=n(3);t.config=i; 39 | /*! 40 | * Primary `Assertion` prototype 41 | */ 42 | var s=n(20);t.use(s); 43 | /*! 44 | * Core Assertions 45 | */ 46 | var a=n(19);t.use(a); 47 | /*! 48 | * Expect interface 49 | */ 50 | var c=n(18);t.use(c); 51 | /*! 52 | * Should interface 53 | */ 54 | var u=n(17);t.use(u); 55 | /*! 56 | * Assert interface 57 | */ 58 | var f=n(16);t.use(f)},function(e,t){e.exports={includeStack:!1,showDiff:!0,truncateThreshold:40,useProxy:!0,proxyExcludedKeys:["then","catch","inspect","toJSON"]}},function(e,t,n){var o=n(3),r=n(0),i=n(10),s=n(6),a=["__flags","__methods","_obj","assert"];e.exports=function(e,t){return s()?new Proxy(e,{get:function e(n,s){if("string"==typeof s&&-1===o.proxyExcludedKeys.indexOf(s)&&!Reflect.has(n,s)){if(t)throw Error("Invalid Chai property: "+t+"."+s+'. See docs for proper usage of "'+t+'".');var c=null,u=4;throw i(n).forEach(function(e){if(!Object.prototype.hasOwnProperty(e)&&-1===a.indexOf(e)){var t=function(e,t,n){if(Math.abs(e.length-t.length)>=n)return n;for(var o=[],r=0;r<=e.length;r++)o[r]=Array(t.length+1).fill(0),o[r][0]=r;for(var i=0;i=n?o[r][i]=n:o[r][i]=Math.min(o[r-1][i]+1,o[r][i-1]+1,o[r-1][i-1]+(s===t.charCodeAt(i-1)?0:1));return o[e.length][t.length]}(s,e,u);t 62 | * MIT Licensed 63 | */e.exports=function(e,t,o){return n.configurable?(Object.defineProperty(e,"length",{get:function(){if(o)throw Error("Invalid Chai property: "+t+'.length. Due to a compatibility issue, "length" cannot directly follow "'+t+'". Use "'+t+'.lengthOf" instead.');throw Error("Invalid Chai property: "+t+'.length. See docs for proper usage of "'+t+'".')}}),e):e}},function(e,t,n){var o=n(3); 64 | /*! 65 | * Chai - isProxyEnabled helper 66 | * Copyright(c) 2012-2014 Jake Luer 67 | * MIT Licensed 68 | */e.exports=function(){return o.useProxy&&"undefined"!=typeof Proxy&&"undefined"!=typeof Reflect}},function(e,t,n){var o=n(11),r=n(10),i=n(32),s=n(3);e.exports=function(e,t,n,o){return c({showHidden:t,seen:[],stylize:function(e){return e}},e,void 0===n?2:n)};var a=function(e){return"object"==typeof HTMLElement?e instanceof HTMLElement:e&&"object"==typeof e&&"nodeType"in e&&1===e.nodeType&&"string"==typeof e.nodeName};function c(e,n,y){if(n&&"function"==typeof n.inspect&&n.inspect!==t.inspect&&(!n.constructor||n.constructor.prototype!==n)){var b=n.inspect(y,e);return"string"!=typeof b&&(b=c(e,b,y)),b}var m=function(e,t){switch(typeof t){case"undefined":return e.stylize("undefined","undefined");case"string":var n="'"+JSON.stringify(t).replace(/^"|"$/g,"").replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return e.stylize(n,"string");case"number":return 0===t&&1/t==-1/0?e.stylize("-0","number"):e.stylize(""+t,"number");case"boolean":return e.stylize(""+t,"boolean");case"symbol":return e.stylize(t.toString(),"symbol")}if(null===t)return e.stylize("null","null")}(e,n);if(m)return m;if(a(n)){if("outerHTML"in n)return n.outerHTML;try{if(document.xmlVersion)return(new XMLSerializer).serializeToString(n);var w=document.createElementNS("http://www.w3.org/1999/xhtml","_");w.appendChild(n.cloneNode(!1));var g=w.innerHTML.replace("><",">"+n.innerHTML+"<");return w.innerHTML="",g}catch(e){}}var v,x,O=i(n),j=e.showHidden?r(n):O;if(0===j.length||l(n)&&(1===j.length&&"stack"===j[0]||2===j.length&&"description"===j[0]&&"stack"===j[1])){if("function"==typeof n)return x=(v=o(n))?": "+v:"",e.stylize("[Function"+x+"]","special");if(p(n))return e.stylize(RegExp.prototype.toString.call(n),"regexp");if(d(n))return e.stylize(Date.prototype.toUTCString.call(n),"date");if(l(n))return u(n)}var M,S,N="",P=!1,k=!1,E=["{","}"];if("object"==typeof(M=n)&&/\w+Array]$/.test(h(M))&&(k=!0,E=["[","]"]),function(e){return Array.isArray(e)||"object"==typeof e&&"[object Array]"===h(e)}(n)&&(P=!0,E=["[","]"]),"function"==typeof n&&(N=" [Function"+(x=(v=o(n))?": "+v:"")+"]"),p(n)&&(N=" "+RegExp.prototype.toString.call(n)),d(n)&&(N=" "+Date.prototype.toUTCString.call(n)),l(n))return u(n);if(0===j.length&&(!P||0==n.length))return E[0]+N+E[1];if(y<0)return p(n)?e.stylize(RegExp.prototype.toString.call(n),"regexp"):e.stylize("[Object]","special");if(e.seen.push(n),P)S=function(e,t,n,o,r){for(var i=[],s=0,a=t.length;s=s.truncateThreshold-7){t+="...";break}t+=e[n]+", "}-1!==(t+=" ]").indexOf(", ]")&&(t=t.replace(", ]"," ]"));return t}(n);S=j.map(function(t){return f(e,n,y,O,t,P)})}return e.seen.pop(),function(e,t,n){if(e.reduce(function(e,t){return e+t.length+1},0)>60)return n[0]+(""===t?"":t+"\n ")+" "+e.join(",\n ")+" "+n[1];return n[0]+t+" "+e.join(", ")+" "+n[1]}(S,N,E)}function u(e){return"["+Error.prototype.toString.call(e)+"]"}function f(e,t,n,o,r,i){var s,a,u=Object.getOwnPropertyDescriptor(t,r);if(u&&(u.get?a=u.set?e.stylize("[Getter/Setter]","special"):e.stylize("[Getter]","special"):u.set&&(a=e.stylize("[Setter]","special"))),o.indexOf(r)<0&&(s="["+r+"]"),a||(e.seen.indexOf(t[r])<0?(a=c(e,t[r],null===n?null:n-1)).indexOf("\n")>-1&&(a=i?a.split("\n").map(function(e){return" "+e}).join("\n").substr(2):"\n"+a.split("\n").map(function(e){return" "+e}).join("\n")):a=e.stylize("[Circular]","special")),void 0===s){if(i&&r.match(/^\d+$/))return a;(s=JSON.stringify(""+r)).match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)?(s=s.substr(1,s.length-2),s=e.stylize(s,"name")):(s=s.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'"),s=e.stylize(s,"string"))}return s+": "+a}function p(e){return"object"==typeof e&&"[object RegExp]"===h(e)}function d(e){return"object"==typeof e&&"[object Date]"===h(e)}function l(e){return"object"==typeof e&&"[object Error]"===h(e)}function h(e){return Object.prototype.toString.call(e)}},function(e,t,n){(function(t){var n;n=function(){"use strict";var e="function"==typeof Promise,n="object"==typeof self?self:t,o="undefined"!=typeof Symbol,r="undefined"!=typeof Map,i="undefined"!=typeof Set,s="undefined"!=typeof WeakMap,a="undefined"!=typeof WeakSet,c="undefined"!=typeof DataView,u=o&&void 0!==Symbol.iterator,f=o&&void 0!==Symbol.toStringTag,p=i&&"function"==typeof Set.prototype.entries,d=r&&"function"==typeof Map.prototype.entries,l=p&&Object.getPrototypeOf((new Set).entries()),h=d&&Object.getPrototypeOf((new Map).entries()),y=u&&"function"==typeof Array.prototype[Symbol.iterator],b=y&&Object.getPrototypeOf([][Symbol.iterator]()),m=u&&"function"==typeof String.prototype[Symbol.iterator],w=m&&Object.getPrototypeOf(""[Symbol.iterator]()),g=8,v=-1;return function(t){var o=typeof t;if("object"!==o)return o;if(null===t)return"null";if(t===n)return"global";if(Array.isArray(t)&&(!1===f||!(Symbol.toStringTag in t)))return"Array";if("object"==typeof window&&null!==window){if("object"==typeof window.location&&t===window.location)return"Location";if("object"==typeof window.document&&t===window.document)return"Document";if("object"==typeof window.navigator){if("object"==typeof window.navigator.mimeTypes&&t===window.navigator.mimeTypes)return"MimeTypeArray";if("object"==typeof window.navigator.plugins&&t===window.navigator.plugins)return"PluginArray"}if(("function"==typeof window.HTMLElement||"object"==typeof window.HTMLElement)&&t instanceof window.HTMLElement){if("BLOCKQUOTE"===t.tagName)return"HTMLQuoteElement";if("TD"===t.tagName)return"HTMLTableDataCellElement";if("TH"===t.tagName)return"HTMLTableHeaderCellElement"}}var u=f&&t[Symbol.toStringTag];if("string"==typeof u)return u;var p=Object.getPrototypeOf(t);return p===RegExp.prototype?"RegExp":p===Date.prototype?"Date":e&&p===Promise.prototype?"Promise":i&&p===Set.prototype?"Set":r&&p===Map.prototype?"Map":a&&p===WeakSet.prototype?"WeakSet":s&&p===WeakMap.prototype?"WeakMap":c&&p===DataView.prototype?"DataView":r&&p===h?"Map Iterator":i&&p===l?"Set Iterator":y&&p===b?"Array Iterator":m&&p===w?"String Iterator":null===p?"Object":Object.prototype.toString.call(t).slice(g,v)}},e.exports=n()}).call(this,n(35))},function(e,t){ 69 | /*! 70 | * Chai - getOwnEnumerablePropertySymbols utility 71 | * Copyright(c) 2011-2016 Jake Luer 72 | * MIT Licensed 73 | */ 74 | e.exports=function(e){return"function"!=typeof Object.getOwnPropertySymbols?[]:Object.getOwnPropertySymbols(e).filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})}},function(e,t){ 75 | /*! 76 | * Chai - getProperties utility 77 | * Copyright(c) 2012-2014 Jake Luer 78 | * MIT Licensed 79 | */ 80 | e.exports=function(e){var t=Object.getOwnPropertyNames(e);function n(e){-1===t.indexOf(e)&&t.push(e)}for(var o=Object.getPrototypeOf(e);null!==o;)Object.getOwnPropertyNames(o).forEach(n),o=Object.getPrototypeOf(o);return t}},function(e,t,n){"use strict";var o=Function.prototype.toString,r=/\s*function(?:\s|\s*\/\*[^(?:*\/)]+\*\/\s*)*([^\s\(\/]+)/;e.exports=function(e){if("function"!=typeof e)return null;var t="";if(void 0===Function.prototype.name&&void 0===e.name){var n=o.call(e).match(r);n&&(t=n[1])}else t=e.name;return t}},function(e,t,n){ 81 | /*! 82 | * Chai - flag utility 83 | * Copyright(c) 2012-2014 Jake Luer 84 | * MIT Licensed 85 | */ 86 | /*! 87 | * Module dependencies 88 | */ 89 | var o=n(7),r=n(3);e.exports=function(e){var t=o(e),n=Object.prototype.toString.call(e);if(r.truncateThreshold&&t.length>=r.truncateThreshold){if("[object Function]"===n)return e.name&&""!==e.name?"[Function: "+e.name+"]":"[Function]";if("[object Array]"===n)return"[ Array("+e.length+") ]";if("[object Object]"===n){var i=Object.keys(e);return"{ Object ("+(i.length>2?i.splice(0,2).join(", ")+", ...":i.join(", "))+") }"}return t}return t}},function(e,t){ 90 | /*! 91 | * Chai - getActual utility 92 | * Copyright(c) 2012-2014 Jake Luer 93 | * MIT Licensed 94 | */ 95 | e.exports=function(e,t){return t.length>4?t[4]:e._obj}},function(e,t){ 96 | /*! 97 | * assertion-error 98 | * Copyright(c) 2013 Jake Luer 99 | * MIT Licensed 100 | */ 101 | /*! 102 | * Return a function that will copy properties from 103 | * one object to another excluding any originally 104 | * listed. Returned function will create a new `{}`. 105 | * 106 | * @param {String} excluded properties ... 107 | * @return {Function} 108 | */ 109 | function n(){var e=[].slice.call(arguments);function t(t,n){Object.keys(n).forEach(function(o){~e.indexOf(o)||(t[o]=n[o])})}return function(){for(var e=[].slice.call(arguments),n=0,o={};n 128 | * MIT Licensed 129 | */ 130 | e.exports=function(e,t){ 131 | /*! 132 | * Chai dependencies. 133 | */ 134 | var n=e.Assertion,o=t.flag,r=e.assert=function(t,o){new n(null,null,e.assert,!0).assert(t,o,"[ negation message unavailable ]")}; 135 | /*! 136 | * Module export. 137 | */r.fail=function(t,n,o,i){throw arguments.length<2&&(o=t,t=void 0),o=o||"assert.fail()",new e.AssertionError(o,{actual:t,expected:n,operator:i},r.fail)},r.isOk=function(e,t){new n(e,t,r.isOk,!0).is.ok},r.isNotOk=function(e,t){new n(e,t,r.isNotOk,!0).is.not.ok},r.equal=function(e,t,i){var s=new n(e,i,r.equal,!0);s.assert(t==o(s,"object"),"expected #{this} to equal #{exp}","expected #{this} to not equal #{act}",t,e,!0)},r.notEqual=function(e,t,i){var s=new n(e,i,r.notEqual,!0);s.assert(t!=o(s,"object"),"expected #{this} to not equal #{exp}","expected #{this} to equal #{act}",t,e,!0)},r.strictEqual=function(e,t,o){new n(e,o,r.strictEqual,!0).to.equal(t)},r.notStrictEqual=function(e,t,o){new n(e,o,r.notStrictEqual,!0).to.not.equal(t)},r.deepEqual=r.deepStrictEqual=function(e,t,o){new n(e,o,r.deepEqual,!0).to.eql(t)},r.notDeepEqual=function(e,t,o){new n(e,o,r.notDeepEqual,!0).to.not.eql(t)},r.isAbove=function(e,t,o){new n(e,o,r.isAbove,!0).to.be.above(t)},r.isAtLeast=function(e,t,o){new n(e,o,r.isAtLeast,!0).to.be.least(t)},r.isBelow=function(e,t,o){new n(e,o,r.isBelow,!0).to.be.below(t)},r.isAtMost=function(e,t,o){new n(e,o,r.isAtMost,!0).to.be.most(t)},r.isTrue=function(e,t){new n(e,t,r.isTrue,!0).is.true},r.isNotTrue=function(e,t){new n(e,t,r.isNotTrue,!0).to.not.equal(!0)},r.isFalse=function(e,t){new n(e,t,r.isFalse,!0).is.false},r.isNotFalse=function(e,t){new n(e,t,r.isNotFalse,!0).to.not.equal(!1)},r.isNull=function(e,t){new n(e,t,r.isNull,!0).to.equal(null)},r.isNotNull=function(e,t){new n(e,t,r.isNotNull,!0).to.not.equal(null)},r.isNaN=function(e,t){new n(e,t,r.isNaN,!0).to.be.NaN},r.isNotNaN=function(e,t){new n(e,t,r.isNotNaN,!0).not.to.be.NaN},r.exists=function(e,t){new n(e,t,r.exists,!0).to.exist},r.notExists=function(e,t){new n(e,t,r.notExists,!0).to.not.exist},r.isUndefined=function(e,t){new n(e,t,r.isUndefined,!0).to.equal(void 0)},r.isDefined=function(e,t){new n(e,t,r.isDefined,!0).to.not.equal(void 0)},r.isFunction=function(e,t){new n(e,t,r.isFunction,!0).to.be.a("function")},r.isNotFunction=function(e,t){new n(e,t,r.isNotFunction,!0).to.not.be.a("function")},r.isObject=function(e,t){new n(e,t,r.isObject,!0).to.be.a("object")},r.isNotObject=function(e,t){new n(e,t,r.isNotObject,!0).to.not.be.a("object")},r.isArray=function(e,t){new n(e,t,r.isArray,!0).to.be.an("array")},r.isNotArray=function(e,t){new n(e,t,r.isNotArray,!0).to.not.be.an("array")},r.isString=function(e,t){new n(e,t,r.isString,!0).to.be.a("string")},r.isNotString=function(e,t){new n(e,t,r.isNotString,!0).to.not.be.a("string")},r.isNumber=function(e,t){new n(e,t,r.isNumber,!0).to.be.a("number")},r.isNotNumber=function(e,t){new n(e,t,r.isNotNumber,!0).to.not.be.a("number")},r.isFinite=function(e,t){new n(e,t,r.isFinite,!0).to.be.finite},r.isBoolean=function(e,t){new n(e,t,r.isBoolean,!0).to.be.a("boolean")},r.isNotBoolean=function(e,t){new n(e,t,r.isNotBoolean,!0).to.not.be.a("boolean")},r.typeOf=function(e,t,o){new n(e,o,r.typeOf,!0).to.be.a(t)},r.notTypeOf=function(e,t,o){new n(e,o,r.notTypeOf,!0).to.not.be.a(t)},r.instanceOf=function(e,t,o){new n(e,o,r.instanceOf,!0).to.be.instanceOf(t)},r.notInstanceOf=function(e,t,o){new n(e,o,r.notInstanceOf,!0).to.not.be.instanceOf(t)},r.include=function(e,t,o){new n(e,o,r.include,!0).include(t)},r.notInclude=function(e,t,o){new n(e,o,r.notInclude,!0).not.include(t)},r.deepInclude=function(e,t,o){new n(e,o,r.deepInclude,!0).deep.include(t)},r.notDeepInclude=function(e,t,o){new n(e,o,r.notDeepInclude,!0).not.deep.include(t)},r.nestedInclude=function(e,t,o){new n(e,o,r.nestedInclude,!0).nested.include(t)},r.notNestedInclude=function(e,t,o){new n(e,o,r.notNestedInclude,!0).not.nested.include(t)},r.deepNestedInclude=function(e,t,o){new n(e,o,r.deepNestedInclude,!0).deep.nested.include(t)},r.notDeepNestedInclude=function(e,t,o){new n(e,o,r.notDeepNestedInclude,!0).not.deep.nested.include(t)},r.ownInclude=function(e,t,o){new n(e,o,r.ownInclude,!0).own.include(t)},r.notOwnInclude=function(e,t,o){new n(e,o,r.notOwnInclude,!0).not.own.include(t)},r.deepOwnInclude=function(e,t,o){new n(e,o,r.deepOwnInclude,!0).deep.own.include(t)},r.notDeepOwnInclude=function(e,t,o){new n(e,o,r.notDeepOwnInclude,!0).not.deep.own.include(t)},r.match=function(e,t,o){new n(e,o,r.match,!0).to.match(t)},r.notMatch=function(e,t,o){new n(e,o,r.notMatch,!0).to.not.match(t)},r.property=function(e,t,o){new n(e,o,r.property,!0).to.have.property(t)},r.notProperty=function(e,t,o){new n(e,o,r.notProperty,!0).to.not.have.property(t)},r.propertyVal=function(e,t,o,i){new n(e,i,r.propertyVal,!0).to.have.property(t,o)},r.notPropertyVal=function(e,t,o,i){new n(e,i,r.notPropertyVal,!0).to.not.have.property(t,o)},r.deepPropertyVal=function(e,t,o,i){new n(e,i,r.deepPropertyVal,!0).to.have.deep.property(t,o)},r.notDeepPropertyVal=function(e,t,o,i){new n(e,i,r.notDeepPropertyVal,!0).to.not.have.deep.property(t,o)},r.ownProperty=function(e,t,o){new n(e,o,r.ownProperty,!0).to.have.own.property(t)},r.notOwnProperty=function(e,t,o){new n(e,o,r.notOwnProperty,!0).to.not.have.own.property(t)},r.ownPropertyVal=function(e,t,o,i){new n(e,i,r.ownPropertyVal,!0).to.have.own.property(t,o)},r.notOwnPropertyVal=function(e,t,o,i){new n(e,i,r.notOwnPropertyVal,!0).to.not.have.own.property(t,o)},r.deepOwnPropertyVal=function(e,t,o,i){new n(e,i,r.deepOwnPropertyVal,!0).to.have.deep.own.property(t,o)},r.notDeepOwnPropertyVal=function(e,t,o,i){new n(e,i,r.notDeepOwnPropertyVal,!0).to.not.have.deep.own.property(t,o)},r.nestedProperty=function(e,t,o){new n(e,o,r.nestedProperty,!0).to.have.nested.property(t)},r.notNestedProperty=function(e,t,o){new n(e,o,r.notNestedProperty,!0).to.not.have.nested.property(t)},r.nestedPropertyVal=function(e,t,o,i){new n(e,i,r.nestedPropertyVal,!0).to.have.nested.property(t,o)},r.notNestedPropertyVal=function(e,t,o,i){new n(e,i,r.notNestedPropertyVal,!0).to.not.have.nested.property(t,o)},r.deepNestedPropertyVal=function(e,t,o,i){new n(e,i,r.deepNestedPropertyVal,!0).to.have.deep.nested.property(t,o)},r.notDeepNestedPropertyVal=function(e,t,o,i){new n(e,i,r.notDeepNestedPropertyVal,!0).to.not.have.deep.nested.property(t,o)},r.lengthOf=function(e,t,o){new n(e,o,r.lengthOf,!0).to.have.lengthOf(t)},r.hasAnyKeys=function(e,t,o){new n(e,o,r.hasAnyKeys,!0).to.have.any.keys(t)},r.hasAllKeys=function(e,t,o){new n(e,o,r.hasAllKeys,!0).to.have.all.keys(t)},r.containsAllKeys=function(e,t,o){new n(e,o,r.containsAllKeys,!0).to.contain.all.keys(t)},r.doesNotHaveAnyKeys=function(e,t,o){new n(e,o,r.doesNotHaveAnyKeys,!0).to.not.have.any.keys(t)},r.doesNotHaveAllKeys=function(e,t,o){new n(e,o,r.doesNotHaveAllKeys,!0).to.not.have.all.keys(t)},r.hasAnyDeepKeys=function(e,t,o){new n(e,o,r.hasAnyDeepKeys,!0).to.have.any.deep.keys(t)},r.hasAllDeepKeys=function(e,t,o){new n(e,o,r.hasAllDeepKeys,!0).to.have.all.deep.keys(t)},r.containsAllDeepKeys=function(e,t,o){new n(e,o,r.containsAllDeepKeys,!0).to.contain.all.deep.keys(t)},r.doesNotHaveAnyDeepKeys=function(e,t,o){new n(e,o,r.doesNotHaveAnyDeepKeys,!0).to.not.have.any.deep.keys(t)},r.doesNotHaveAllDeepKeys=function(e,t,o){new n(e,o,r.doesNotHaveAllDeepKeys,!0).to.not.have.all.deep.keys(t)},r.throws=function(e,t,i,s){("string"==typeof t||t instanceof RegExp)&&(i=t,t=null);var a=new n(e,s,r.throws,!0).to.throw(t,i);return o(a,"object")},r.doesNotThrow=function(e,t,o,i){("string"==typeof t||t instanceof RegExp)&&(o=t,t=null),new n(e,i,r.doesNotThrow,!0).to.not.throw(t,o)},r.operator=function(i,s,a,c){var u;switch(s){case"==":u=i==a;break;case"===":u=i===a;break;case">":u=i>a;break;case">=":u=i>=a;break;case"<":u=i 160 | * MIT Licensed 161 | */ 162 | e.exports=function(e,t){var n=e.Assertion;function o(){Object.defineProperty(Object.prototype,"should",{set:function(e){Object.defineProperty(this,"should",{value:e,enumerable:!0,configurable:!0,writable:!0})},get:function e(){return this instanceof String||this instanceof Number||this instanceof Boolean||"function"==typeof Symbol&&this instanceof Symbol?new n(this.valueOf(),null,e):new n(this,null,e)},configurable:!0});var t={fail:function(n,o,r,i){throw arguments.length<2&&(r=n,n=void 0),r=r||"should.fail()",new e.AssertionError(r,{actual:n,expected:o,operator:i},t.fail)},equal:function(e,t,o){new n(e,o).to.equal(t)},Throw:function(e,t,o,r){new n(e,r).to.Throw(t,o)},exist:function(e,t){new n(e,t).to.exist},not:{}};return t.not.equal=function(e,t,o){new n(e,o).to.not.equal(t)},t.not.Throw=function(e,t,o,r){new n(e,r).to.not.Throw(t,o)},t.not.exist=function(e,t){new n(e,t).to.not.exist},t.throw=t.Throw,t.not.throw=t.not.Throw,t}e.should=o,e.Should=o}},function(e,t){ 163 | /*! 164 | * chai 165 | * Copyright(c) 2011-2014 Jake Luer 166 | * MIT Licensed 167 | */ 168 | e.exports=function(e,t){e.expect=function(t,n){return new e.Assertion(t,n)},e.expect.fail=function(t,n,o,r){throw arguments.length<2&&(o=t,t=void 0),o=o||"expect.fail()",new e.AssertionError(o,{actual:t,expected:n,operator:r},e.expect.fail)}}},function(e,t){ 169 | /*! 170 | * chai 171 | * http://chaijs.com 172 | * Copyright(c) 2011-2014 Jake Luer 173 | * MIT Licensed 174 | */ 175 | e.exports=function(e,t){var n=e.Assertion,o=e.AssertionError,r=t.flag;function i(e,n){n&&r(this,"message",n),e=e.toLowerCase();var o=r(this,"object"),i=~["a","e","i","o","u"].indexOf(e.charAt(0))?"an ":"a ";this.assert(e===t.type(o).toLowerCase(),"expected #{this} to be "+i+e,"expected #{this} not to be "+i+e)}function s(e,n){return t.isNaN(e)&&t.isNaN(n)||e===n}function a(){r(this,"contains",!0)}function c(e,i){i&&r(this,"message",i);var a=r(this,"object"),c=t.type(a).toLowerCase(),u=r(this,"message"),f=r(this,"negate"),p=r(this,"ssfi"),d=r(this,"deep"),l=d?"deep ":"";u=u?u+": ":"";var h=!1;switch(c){case"string":h=-1!==a.indexOf(e);break;case"weakset":if(d)throw new o(u+"unable to use .deep.include with WeakSet",void 0,p);h=a.has(e);break;case"map":var y=d?t.eql:s;a.forEach(function(t){h=h||y(t,e)});break;case"set":d?a.forEach(function(n){h=h||t.eql(n,e)}):h=a.has(e);break;case"array":h=d?a.some(function(n){return t.eql(n,e)}):-1!==a.indexOf(e);break;default:if(e!==Object(e))throw new o(u+"object tested must be an array, a map, an object, a set, a string, or a weakset, but "+c+" given",void 0,p);var b=Object.keys(e),m=null,w=0;if(b.forEach(function(i){var s=new n(a);if(t.transferFlags(this,s,!0),r(s,"lockSsfi",!0),f&&1!==b.length)try{s.property(i,e[i])}catch(e){if(!t.checkError.compatibleConstructor(e,o))throw e;null===m&&(m=e),w++}else s.property(i,e[i])},this),f&&b.length>1&&w===b.length)throw m;return}this.assert(h,"expected #{this} to "+l+"include "+t.inspect(e),"expected #{this} to not "+l+"include "+t.inspect(e))}function u(){var e=r(this,"object"),n=t.type(e);this.assert("Arguments"===n,"expected #{this} to be arguments but got "+n,"expected #{this} to not be arguments")}function f(e,t){t&&r(this,"message",t);var n=r(this,"object");if(r(this,"deep")){var o=r(this,"lockSsfi");r(this,"lockSsfi",!0),this.eql(e),r(this,"lockSsfi",o)}else this.assert(e===n,"expected #{this} to equal #{exp}","expected #{this} to not equal #{exp}",e,this._obj,!0)}function p(e,n){n&&r(this,"message",n),this.assert(t.eql(e,r(this,"object")),"expected #{this} to deeply equal #{exp}","expected #{this} to not deeply equal #{exp}",e,this._obj,!0)}function d(e,i){i&&r(this,"message",i);var s,a=r(this,"object"),c=r(this,"doLength"),u=r(this,"message"),f=u?u+": ":"",p=r(this,"ssfi"),d=t.type(a).toLowerCase(),l=t.type(e).toLowerCase(),h=!0;if(c&&"map"!==d&&"set"!==d&&new n(a,u,p,!0).to.have.property("length"),c||"date"!==d||"date"===l)if("number"===l||!c&&"number"!==d)if(c||"date"===d||"number"===d)h=!1;else{s=f+"expected "+("string"===d?"'"+a+"'":a)+" to be a number or a date"}else s=f+"the argument to above must be a number";else s=f+"the argument to above must be a date";if(h)throw new o(s,void 0,p);if(c){var y,b="length";"map"===d||"set"===d?(b="size",y=a.size):y=a.length,this.assert(y>e,"expected #{this} to have a "+b+" above #{exp} but got #{act}","expected #{this} to not have a "+b+" above #{exp}",e,y)}else this.assert(a>e,"expected #{this} to be above #{exp}","expected #{this} to be at most #{exp}",e)}function l(e,i){i&&r(this,"message",i);var s,a=r(this,"object"),c=r(this,"doLength"),u=r(this,"message"),f=u?u+": ":"",p=r(this,"ssfi"),d=t.type(a).toLowerCase(),l=t.type(e).toLowerCase(),h=!0;if(c&&"map"!==d&&"set"!==d&&new n(a,u,p,!0).to.have.property("length"),c||"date"!==d||"date"===l)if("number"===l||!c&&"number"!==d)if(c||"date"===d||"number"===d)h=!1;else{s=f+"expected "+("string"===d?"'"+a+"'":a)+" to be a number or a date"}else s=f+"the argument to least must be a number";else s=f+"the argument to least must be a date";if(h)throw new o(s,void 0,p);if(c){var y,b="length";"map"===d||"set"===d?(b="size",y=a.size):y=a.length,this.assert(y>=e,"expected #{this} to have a "+b+" at least #{exp} but got #{act}","expected #{this} to have a "+b+" below #{exp}",e,y)}else this.assert(a>=e,"expected #{this} to be at least #{exp}","expected #{this} to be below #{exp}",e)}function h(e,i){i&&r(this,"message",i);var s,a=r(this,"object"),c=r(this,"doLength"),u=r(this,"message"),f=u?u+": ":"",p=r(this,"ssfi"),d=t.type(a).toLowerCase(),l=t.type(e).toLowerCase(),h=!0;if(c&&"map"!==d&&"set"!==d&&new n(a,u,p,!0).to.have.property("length"),c||"date"!==d||"date"===l)if("number"===l||!c&&"number"!==d)if(c||"date"===d||"number"===d)h=!1;else{s=f+"expected "+("string"===d?"'"+a+"'":a)+" to be a number or a date"}else s=f+"the argument to below must be a number";else s=f+"the argument to below must be a date";if(h)throw new o(s,void 0,p);if(c){var y,b="length";"map"===d||"set"===d?(b="size",y=a.size):y=a.length,this.assert(y1&&this.assert(d&&(l?t.eql(n,b):n===b),"expected #{this} to have "+m+t.inspect(e)+" of #{exp}, but got #{act}","expected #{this} to not have "+m+t.inspect(e)+" of #{act}",n,b),r(this,"object",b)}function w(e,t,n){r(this,"own",!0),m.apply(this,arguments)}function g(e,n,o){"string"==typeof n&&(o=n,n=null),o&&r(this,"message",o);var i=r(this,"object"),s=Object.getOwnPropertyDescriptor(Object(i),e);s&&n?this.assert(t.eql(n,s),"expected the own property descriptor for "+t.inspect(e)+" on #{this} to match "+t.inspect(n)+", got "+t.inspect(s),"expected the own property descriptor for "+t.inspect(e)+" on #{this} to not match "+t.inspect(n),n,s,!0):this.assert(s,"expected #{this} to have an own property descriptor for "+t.inspect(e),"expected #{this} to not have an own property descriptor for "+t.inspect(e)),r(this,"object",s)}function v(){r(this,"doLength",!0)}function x(e,o){o&&r(this,"message",o);var i,s=r(this,"object"),a=t.type(s).toLowerCase(),c=r(this,"message"),u=r(this,"ssfi"),f="length";switch(a){case"map":case"set":f="size",i=s.size;break;default:new n(s,c,u,!0).to.have.property("length"),i=s.length}this.assert(i==e,"expected #{this} to have a "+f+" of #{exp} but got #{act}","expected #{this} to not have a "+f+" of #{act}",e,i)}function O(e,t){t&&r(this,"message",t);var n=r(this,"object");this.assert(e.exec(n),"expected #{this} to match "+e,"expected #{this} not to match "+e)}function j(e){var n,i,s=r(this,"object"),a=t.type(s),c=t.type(e),u=r(this,"ssfi"),f=r(this,"deep"),p="",d=!0,l=r(this,"message"),h=(l=l?l+": ":"")+"when testing keys against an object or an array you must give a single Array|Object|String argument or multiple String arguments";if("Map"===a||"Set"===a)p=f?"deeply ":"",i=[],s.forEach(function(e,t){i.push(t)}),"Array"!==c&&(e=Array.prototype.slice.call(arguments));else{switch(i=t.getOwnEnumerableProperties(s),c){case"Array":if(arguments.length>1)throw new o(h,void 0,u);break;case"Object":if(arguments.length>1)throw new o(h,void 0,u);e=Object.keys(e);break;default:e=Array.prototype.slice.call(arguments)}e=e.map(function(e){return"symbol"==typeof e?e:String(e)})}if(!e.length)throw new o(l+"keys required",void 0,u);var y=e.length,b=r(this,"any"),m=r(this,"all"),w=e;if(b||m||(m=!0),b&&(d=w.some(function(e){return i.some(function(n){return f?t.eql(e,n):e===n})})),m&&(d=w.every(function(e){return i.some(function(n){return f?t.eql(e,n):e===n})}),r(this,"contains")||(d=d&&e.length==i.length)),y>1){var g=(e=e.map(function(e){return t.inspect(e)})).pop();m&&(n=e.join(", ")+", and "+g),b&&(n=e.join(", ")+", or "+g)}else n=t.inspect(e[0]);n=(y>1?"keys ":"key ")+n,n=(r(this,"contains")?"contain ":"have ")+n,this.assert(d,"expected #{this} to "+p+n,"expected #{this} to not "+p+n,w.slice(0).sort(t.compareByInspect),i.sort(t.compareByInspect),!0)}function M(e,o,i){i&&r(this,"message",i);var s,a=r(this,"object"),c=r(this,"ssfi"),u=r(this,"message"),f=r(this,"negate")||!1;new n(a,u,c,!0).is.a("function"),(e instanceof RegExp||"string"==typeof e)&&(o=e,e=null);try{a()}catch(e){s=e}var p=void 0===e&&void 0===o,d=Boolean(e&&o),l=!1,h=!1;if(p||!p&&!f){var y="an error";e instanceof Error?y="#{exp}":e&&(y=t.checkError.getConstructorName(e)),this.assert(s,"expected #{this} to throw "+y,"expected #{this} to not throw an error but #{act} was thrown",e&&e.toString(),s instanceof Error?s.toString():"string"==typeof s?s:s&&t.checkError.getConstructorName(s))}if(e&&s){if(e instanceof Error)t.checkError.compatibleInstance(s,e)===f&&(d&&f?l=!0:this.assert(f,"expected #{this} to throw #{exp} but #{act} was thrown","expected #{this} to not throw #{exp}"+(s&&!f?" but #{act} was thrown":""),e.toString(),s.toString()));t.checkError.compatibleConstructor(s,e)===f&&(d&&f?l=!0:this.assert(f,"expected #{this} to throw #{exp} but #{act} was thrown","expected #{this} to not throw #{exp}"+(s?" but #{act} was thrown":""),e instanceof Error?e.toString():e&&t.checkError.getConstructorName(e),s instanceof Error?s.toString():s&&t.checkError.getConstructorName(s)))}if(s&&void 0!==o&&null!==o){var b="including";o instanceof RegExp&&(b="matching"),t.checkError.compatibleMessage(s,o)===f&&(d&&f?h=!0:this.assert(f,"expected #{this} to throw error "+b+" #{exp} but got #{act}","expected #{this} to throw error not "+b+" #{exp}",o,t.checkError.getMessage(s)))}l&&h&&this.assert(f,"expected #{this} to throw #{exp} but #{act} was thrown","expected #{this} to not throw #{exp}"+(s?" but #{act} was thrown":""),e instanceof Error?e.toString():e&&t.checkError.getConstructorName(e),s instanceof Error?s.toString():s&&t.checkError.getConstructorName(s)),r(this,"object",s)}function S(e,n){n&&r(this,"message",n);var o=r(this,"object"),i=r(this,"itself"),s="function"!=typeof o||i?o[e]:o.prototype[e];this.assert("function"==typeof s,"expected #{this} to respond to "+t.inspect(e),"expected #{this} to not respond to "+t.inspect(e))}function N(e,n){n&&r(this,"message",n);var o=e(r(this,"object"));this.assert(o,"expected #{this} to satisfy "+t.objDisplay(e),"expected #{this} to not satisfy"+t.objDisplay(e),!r(this,"negate"),o)}function P(e,t,i){i&&r(this,"message",i);var s=r(this,"object"),a=r(this,"message"),c=r(this,"ssfi");if(new n(s,a,c,!0).is.a("number"),"number"!=typeof e||"number"!=typeof t)throw new o((a=a?a+": ":"")+"the arguments to closeTo or approximately must be numbers",void 0,c);this.assert(Math.abs(s-e)<=t,"expected #{this} to be close to "+e+" +/- "+t,"expected #{this} not to be close to "+e+" +/- "+t)}function k(e,t,o){o&&r(this,"message",o);var i,s=r(this,"object"),a=r(this,"message"),c=r(this,"ssfi");new n(s,a,c,!0).is.a("function"),t?(new n(e,a,c,!0).to.have.property(t),i=e[t]):(new n(e,a,c,!0).is.a("function"),i=e()),s();var u=void 0===t||null===t?e():e[t],f=void 0===t||null===t?i:"."+t;r(this,"deltaMsgObj",f),r(this,"initialDeltaValue",i),r(this,"finalDeltaValue",u),r(this,"deltaBehavior","change"),r(this,"realDelta",u!==i),this.assert(i!==u,"expected "+f+" to change","expected "+f+" to not change")}function E(e,t,o){o&&r(this,"message",o);var i,s=r(this,"object"),a=r(this,"message"),c=r(this,"ssfi");new n(s,a,c,!0).is.a("function"),t?(new n(e,a,c,!0).to.have.property(t),i=e[t]):(new n(e,a,c,!0).is.a("function"),i=e()),new n(i,a,c,!0).is.a("number"),s();var u=void 0===t||null===t?e():e[t],f=void 0===t||null===t?i:"."+t;r(this,"deltaMsgObj",f),r(this,"initialDeltaValue",i),r(this,"finalDeltaValue",u),r(this,"deltaBehavior","increase"),r(this,"realDelta",u-i),this.assert(u-i>0,"expected "+f+" to increase","expected "+f+" to not increase")}function A(e,t,o){o&&r(this,"message",o);var i,s=r(this,"object"),a=r(this,"message"),c=r(this,"ssfi");new n(s,a,c,!0).is.a("function"),t?(new n(e,a,c,!0).to.have.property(t),i=e[t]):(new n(e,a,c,!0).is.a("function"),i=e()),new n(i,a,c,!0).is.a("number"),s();var u=void 0===t||null===t?e():e[t],f=void 0===t||null===t?i:"."+t;r(this,"deltaMsgObj",f),r(this,"initialDeltaValue",i),r(this,"finalDeltaValue",u),r(this,"deltaBehavior","decrease"),r(this,"realDelta",i-u),this.assert(u-i<0,"expected "+f+" to decrease","expected "+f+" to not decrease")}["to","be","been","is","and","has","have","with","that","which","at","of","same","but","does","still"].forEach(function(e){n.addProperty(e)}),n.addProperty("not",function(){r(this,"negate",!0)}),n.addProperty("deep",function(){r(this,"deep",!0)}),n.addProperty("nested",function(){r(this,"nested",!0)}),n.addProperty("own",function(){r(this,"own",!0)}),n.addProperty("ordered",function(){r(this,"ordered",!0)}),n.addProperty("any",function(){r(this,"any",!0),r(this,"all",!1)}),n.addProperty("all",function(){r(this,"all",!0),r(this,"any",!1)}),n.addChainableMethod("an",i),n.addChainableMethod("a",i),n.addChainableMethod("include",c,a),n.addChainableMethod("contain",c,a),n.addChainableMethod("contains",c,a),n.addChainableMethod("includes",c,a),n.addProperty("ok",function(){this.assert(r(this,"object"),"expected #{this} to be truthy","expected #{this} to be falsy")}),n.addProperty("true",function(){this.assert(!0===r(this,"object"),"expected #{this} to be true","expected #{this} to be false",!r(this,"negate"))}),n.addProperty("false",function(){this.assert(!1===r(this,"object"),"expected #{this} to be false","expected #{this} to be true",!!r(this,"negate"))}),n.addProperty("null",function(){this.assert(null===r(this,"object"),"expected #{this} to be null","expected #{this} not to be null")}),n.addProperty("undefined",function(){this.assert(void 0===r(this,"object"),"expected #{this} to be undefined","expected #{this} not to be undefined")}),n.addProperty("NaN",function(){this.assert(t.isNaN(r(this,"object")),"expected #{this} to be NaN","expected #{this} not to be NaN")}),n.addProperty("exist",function(){var e=r(this,"object");this.assert(null!==e&&void 0!==e,"expected #{this} to exist","expected #{this} to not exist")}),n.addProperty("empty",function(){var e,n=r(this,"object"),i=r(this,"ssfi"),s=r(this,"message");switch(s=s?s+": ":"",t.type(n).toLowerCase()){case"array":case"string":e=n.length;break;case"map":case"set":e=n.size;break;case"weakmap":case"weakset":throw new o(s+".empty was passed a weak collection",void 0,i);case"function":var a=s+".empty was passed a function "+t.getName(n);throw new o(a.trim(),void 0,i);default:if(n!==Object(n))throw new o(s+".empty was passed non-string primitive "+t.inspect(n),void 0,i);e=Object.keys(n).length}this.assert(0===e,"expected #{this} to be empty","expected #{this} not to be empty")}),n.addProperty("arguments",u),n.addProperty("Arguments",u),n.addMethod("equal",f),n.addMethod("equals",f),n.addMethod("eq",f),n.addMethod("eql",p),n.addMethod("eqls",p),n.addMethod("above",d),n.addMethod("gt",d),n.addMethod("greaterThan",d),n.addMethod("least",l),n.addMethod("gte",l),n.addMethod("below",h),n.addMethod("lt",h),n.addMethod("lessThan",h),n.addMethod("most",y),n.addMethod("lte",y),n.addMethod("within",function(e,i,s){s&&r(this,"message",s);var a,c=r(this,"object"),u=r(this,"doLength"),f=r(this,"message"),p=f?f+": ":"",d=r(this,"ssfi"),l=t.type(c).toLowerCase(),h=t.type(e).toLowerCase(),y=t.type(i).toLowerCase(),b=!0,m="date"===h&&"date"===y?e.toUTCString()+".."+i.toUTCString():e+".."+i;if(u&&"map"!==l&&"set"!==l&&new n(c,f,d,!0).to.have.property("length"),u||"date"!==l||"date"===h&&"date"===y)if("number"===h&&"number"===y||!u&&"number"!==l)if(u||"date"===l||"number"===l)b=!1;else{a=p+"expected "+("string"===l?"'"+c+"'":c)+" to be a number or a date"}else a=p+"the arguments to within must be numbers";else a=p+"the arguments to within must be dates";if(b)throw new o(a,void 0,d);if(u){var w,g="length";"map"===l||"set"===l?(g="size",w=c.size):w=c.length,this.assert(w>=e&&w<=i,"expected #{this} to have a "+g+" within "+m,"expected #{this} to not have a "+g+" within "+m)}else this.assert(c>=e&&c<=i,"expected #{this} to be within "+m,"expected #{this} to not be within "+m)}),n.addMethod("instanceof",b),n.addMethod("instanceOf",b),n.addMethod("property",m),n.addMethod("ownProperty",w),n.addMethod("haveOwnProperty",w),n.addMethod("ownPropertyDescriptor",g),n.addMethod("haveOwnPropertyDescriptor",g),n.addChainableMethod("length",x,v),n.addChainableMethod("lengthOf",x,v),n.addMethod("match",O),n.addMethod("matches",O),n.addMethod("string",function(e,o){o&&r(this,"message",o);var i=r(this,"object"),s=r(this,"message"),a=r(this,"ssfi");new n(i,s,a,!0).is.a("string"),this.assert(~i.indexOf(e),"expected #{this} to contain "+t.inspect(e),"expected #{this} to not contain "+t.inspect(e))}),n.addMethod("keys",j),n.addMethod("key",j),n.addMethod("throw",M),n.addMethod("throws",M),n.addMethod("Throw",M),n.addMethod("respondTo",S),n.addMethod("respondsTo",S),n.addProperty("itself",function(){r(this,"itself",!0)}),n.addMethod("satisfy",N),n.addMethod("satisfies",N),n.addMethod("closeTo",P),n.addMethod("approximately",P),n.addMethod("members",function(e,o){o&&r(this,"message",o);var i=r(this,"object"),s=r(this,"message"),a=r(this,"ssfi");new n(i,s,a,!0).to.be.an("array"),new n(e,s,a,!0).to.be.an("array");var c,u,f,p=r(this,"contains"),d=r(this,"ordered");p?(u="expected #{this} to be "+(c=d?"an ordered superset":"a superset")+" of #{exp}",f="expected #{this} to not be "+c+" of #{exp}"):(u="expected #{this} to have the same "+(c=d?"ordered members":"members")+" as #{exp}",f="expected #{this} to not have the same "+c+" as #{exp}");var l=r(this,"deep")?t.eql:void 0;this.assert(function(e,t,n,o,r){if(!o){if(e.length!==t.length)return!1;t=t.slice()}return e.every(function(e,i){if(r)return n?n(e,t[i]):e===t[i];if(!n){var s=t.indexOf(e);return-1!==s&&(o||t.splice(s,1),!0)}return t.some(function(r,i){return!!n(e,r)&&(o||t.splice(i,1),!0)})})}(e,i,l,p,d),u,f,e,i,!0)}),n.addMethod("oneOf",function(e,t){t&&r(this,"message",t);var o=r(this,"object"),i=r(this,"message"),s=r(this,"ssfi");new n(e,i,s,!0).to.be.an("array"),this.assert(e.indexOf(o)>-1,"expected #{this} to be one of #{exp}","expected #{this} to not be one of #{exp}",e,o)}),n.addMethod("change",k),n.addMethod("changes",k),n.addMethod("increase",E),n.addMethod("increases",E),n.addMethod("decrease",A),n.addMethod("decreases",A),n.addMethod("by",function(e,t){t&&r(this,"message",t);var n,o=r(this,"deltaMsgObj"),i=r(this,"initialDeltaValue"),s=r(this,"finalDeltaValue"),a=r(this,"deltaBehavior"),c=r(this,"realDelta");n="change"===a?Math.abs(s-i)===Math.abs(e):c===Math.abs(e),this.assert(n,"expected "+o+" to "+a+" by "+e,"expected "+o+" to not "+a+" by "+e)}),n.addProperty("extensible",function(){var e=r(this,"object"),t=e===Object(e)&&Object.isExtensible(e);this.assert(t,"expected #{this} to be extensible","expected #{this} to not be extensible")}),n.addProperty("sealed",function(){var e=r(this,"object"),t=e!==Object(e)||Object.isSealed(e);this.assert(t,"expected #{this} to be sealed","expected #{this} to not be sealed")}),n.addProperty("frozen",function(){var e=r(this,"object"),t=e!==Object(e)||Object.isFrozen(e);this.assert(t,"expected #{this} to be frozen","expected #{this} to not be frozen")}),n.addProperty("finite",function(e){var t=r(this,"object");this.assert("number"==typeof t&&isFinite(t),"expected #{this} to be a finite number","expected #{this} to not be a finite number")})}},function(e,t,n){ 176 | /*! 177 | * chai 178 | * http://chaijs.com 179 | * Copyright(c) 2011-2014 Jake Luer 180 | * MIT Licensed 181 | */ 182 | var o=n(3);e.exports=function(e,t){ 183 | /*! 184 | * Module dependencies. 185 | */ 186 | var n=e.AssertionError,r=t.flag; 187 | /*! 188 | * Module export. 189 | */ 190 | /*! 191 | * Assertion Constructor 192 | * 193 | * Creates object for chaining. 194 | * 195 | * `Assertion` objects contain metadata in the form of flags. Three flags can 196 | * be assigned during instantiation by passing arguments to this constructor: 197 | * 198 | * - `object`: This flag contains the target of the assertion. For example, in 199 | * the assertion `expect(numKittens).to.equal(7);`, the `object` flag will 200 | * contain `numKittens` so that the `equal` assertion can reference it when 201 | * needed. 202 | * 203 | * - `message`: This flag contains an optional custom error message to be 204 | * prepended to the error message that's generated by the assertion when it 205 | * fails. 206 | * 207 | * - `ssfi`: This flag stands for "start stack function indicator". It 208 | * contains a function reference that serves as the starting point for 209 | * removing frames from the stack trace of the error that's created by the 210 | * assertion when it fails. The goal is to provide a cleaner stack trace to 211 | * end users by removing Chai's internal functions. Note that it only works 212 | * in environments that support `Error.captureStackTrace`, and only when 213 | * `Chai.config.includeStack` hasn't been set to `false`. 214 | * 215 | * - `lockSsfi`: This flag controls whether or not the given `ssfi` flag 216 | * should retain its current value, even as assertions are chained off of 217 | * this object. This is usually set to `true` when creating a new assertion 218 | * from within another assertion. It's also temporarily set to `true` before 219 | * an overwritten assertion gets called by the overwriting assertion. 220 | * 221 | * @param {Mixed} obj target of the assertion 222 | * @param {String} msg (optional) custom error message 223 | * @param {Function} ssfi (optional) starting point for removing stack frames 224 | * @param {Boolean} lockSsfi (optional) whether or not the ssfi flag is locked 225 | * @api private 226 | */ 227 | function i(e,n,o,s){return r(this,"ssfi",o||i),r(this,"lockSsfi",s),r(this,"object",e),r(this,"message",n),t.proxify(this)}e.Assertion=i,Object.defineProperty(i,"includeStack",{get:function(){return console.warn("Assertion.includeStack is deprecated, use chai.config.includeStack instead."),o.includeStack},set:function(e){console.warn("Assertion.includeStack is deprecated, use chai.config.includeStack instead."),o.includeStack=e}}),Object.defineProperty(i,"showDiff",{get:function(){return console.warn("Assertion.showDiff is deprecated, use chai.config.showDiff instead."),o.showDiff},set:function(e){console.warn("Assertion.showDiff is deprecated, use chai.config.showDiff instead."),o.showDiff=e}}),i.addProperty=function(e,n){t.addProperty(this.prototype,e,n)},i.addMethod=function(e,n){t.addMethod(this.prototype,e,n)},i.addChainableMethod=function(e,n,o){t.addChainableMethod(this.prototype,e,n,o)},i.overwriteProperty=function(e,n){t.overwriteProperty(this.prototype,e,n)},i.overwriteMethod=function(e,n){t.overwriteMethod(this.prototype,e,n)},i.overwriteChainableMethod=function(e,n,o){t.overwriteChainableMethod(this.prototype,e,n,o)},i.prototype.assert=function(e,i,s,a,c,u){var f=t.test(this,arguments);if(!1!==u&&(u=!0),void 0===a&&void 0===c&&(u=!1),!0!==o.showDiff&&(u=!1),!f){i=t.getMessage(this,arguments);var p=t.getActual(this,arguments);throw new n(i,{actual:p,expected:a,showDiff:u},o.includeStack?this.assert:r(this,"ssfi"))}}, 228 | /*! 229 | * ### ._obj 230 | * 231 | * Quick reference to stored `actual` value for plugin developers. 232 | * 233 | * @api private 234 | */ 235 | Object.defineProperty(i.prototype,"_obj",{get:function(){return r(this,"object")},set:function(e){r(this,"object",e)}})}},function(e,t){e.exports=Number.isNaN|| 236 | /*! 237 | * Chai - isNaN utility 238 | * Copyright(c) 2012-2015 Sakthipriyan Vairamani 239 | * MIT Licensed 240 | */ 241 | function(e){return e!=e}},function(e,t,n){"use strict";var o=/\s*function(?:\s|\s*\/\*[^(?:*\/)]+\*\/\s*)*([^\(\/]+)/;function r(e){var t="";if(void 0===e.name){var n=String(e).match(o);n&&(t=n[1])}else t=e.name;return t}e.exports={compatibleInstance:function(e,t){return t instanceof Error&&e===t},compatibleConstructor:function(e,t){return t instanceof Error?e.constructor===t.constructor||e instanceof t.constructor:(t.prototype instanceof Error||t===Error)&&(e.constructor===t||e instanceof t)},compatibleMessage:function(e,t){var n="string"==typeof e?e:e.message;return t instanceof RegExp?t.test(n):"string"==typeof t&&-1!==n.indexOf(t)},getMessage:function(e){var t="";return e&&e.message?t=e.message:"string"==typeof e&&(t=e),t},getConstructorName:function(e){var t=e;return e instanceof Error?t=r(e.constructor):"function"==typeof e&&(t=r(e).trim()||r(new e)),t}}},function(e,t,n){ 242 | /*! 243 | * Chai - getOwnEnumerableProperties utility 244 | * Copyright(c) 2011-2016 Jake Luer 245 | * MIT Licensed 246 | */ 247 | /*! 248 | * Module dependencies 249 | */ 250 | var o=n(9);e.exports=function(e){return Object.keys(e).concat(o(e))}},function(e,t,n){ 251 | /*! 252 | * Chai - compareByInspect utility 253 | * Copyright(c) 2011-2016 Jake Luer 254 | * MIT Licensed 255 | */ 256 | /*! 257 | * Module dependencies 258 | */ 259 | var o=n(7);e.exports=function(e,t){return o(e) 263 | * MIT Licensed 264 | */ 265 | var o=n(2),r=n(1);e.exports=function(e,t,n,i){var s=e.__methods[t],a=s.chainingBehavior;s.chainingBehavior=function(){var e=i(a).call(this);if(void 0!==e)return e;var t=new o.Assertion;return r(this,t),t};var c=s.method;s.method=function(){var e=n(c).apply(this,arguments);if(void 0!==e)return e;var t=new o.Assertion;return r(this,t),t}}},function(e,t,n){ 266 | /*! 267 | * Chai - addChainingMethod utility 268 | * Copyright(c) 2012-2014 Jake Luer 269 | * MIT Licensed 270 | */ 271 | /*! 272 | * Module dependencies 273 | */ 274 | var o=n(5),r=n(2),i=n(0),s=n(4),a=n(1),c="function"==typeof Object.setPrototypeOf,u=function(){},f=Object.getOwnPropertyNames(u).filter(function(e){var t=Object.getOwnPropertyDescriptor(u,e);return"object"!=typeof t||!t.configurable}),p=Function.prototype.call,d=Function.prototype.apply;e.exports=function(e,t,n,u){"function"!=typeof u&&(u=function(){});var l={method:n,chainingBehavior:u};e.__methods||(e.__methods={}),e.__methods[t]=l,Object.defineProperty(e,t,{get:function(){l.chainingBehavior.call(this);var n=function(){i(this,"lockSsfi")||i(this,"ssfi",n);var e=l.method.apply(this,arguments);if(void 0!==e)return e;var t=new r.Assertion;return a(this,t),t};if(o(n,t,!0),c){var u=Object.create(this);u.call=p,u.apply=d,Object.setPrototypeOf(n,u)}else{Object.getOwnPropertyNames(e).forEach(function(t){if(-1===f.indexOf(t)){var o=Object.getOwnPropertyDescriptor(e,t);Object.defineProperty(n,t,o)}})}return a(this,n),s(n)},configurable:!0})}},function(e,t,n){ 275 | /*! 276 | * Chai - overwriteMethod utility 277 | * Copyright(c) 2012-2014 Jake Luer 278 | * MIT Licensed 279 | */ 280 | var o=n(5),r=n(2),i=n(0),s=n(4),a=n(1);e.exports=function(e,t,n){var c=e[t],u=function(){throw new Error(t+" is not a function")};c&&"function"==typeof c&&(u=c);var f=function(){i(this,"lockSsfi")||i(this,"ssfi",f);var e=i(this,"lockSsfi");i(this,"lockSsfi",!0);var t=n(u).apply(this,arguments);if(i(this,"lockSsfi",e),void 0!==t)return t;var o=new r.Assertion;return a(this,o),o};o(f,t,!1),e[t]=s(f,t)}},function(e,t,n){ 281 | /*! 282 | * Chai - overwriteProperty utility 283 | * Copyright(c) 2012-2014 Jake Luer 284 | * MIT Licensed 285 | */ 286 | var o=n(2),r=n(0),i=n(6),s=n(1);e.exports=function(e,t,n){var a=Object.getOwnPropertyDescriptor(e,t),c=function(){};a&&"function"==typeof a.get&&(c=a.get),Object.defineProperty(e,t,{get:function e(){i()||r(this,"lockSsfi")||r(this,"ssfi",e);var t=r(this,"lockSsfi");r(this,"lockSsfi",!0);var a=n(c).call(this);if(r(this,"lockSsfi",t),void 0!==a)return a;var u=new o.Assertion;return s(this,u),u},configurable:!0})}},function(e,t,n){ 287 | /*! 288 | * Chai - addMethod utility 289 | * Copyright(c) 2012-2014 Jake Luer 290 | * MIT Licensed 291 | */ 292 | var o=n(5),r=n(2),i=n(0),s=n(4),a=n(1);e.exports=function(e,t,n){var c=function(){i(this,"lockSsfi")||i(this,"ssfi",c);var e=n.apply(this,arguments);if(void 0!==e)return e;var t=new r.Assertion;return a(this,t),t};o(c,t,!1),e[t]=s(c,t)}},function(e,t,n){ 293 | /*! 294 | * Chai - addProperty utility 295 | * Copyright(c) 2012-2014 Jake Luer 296 | * MIT Licensed 297 | */ 298 | var o=n(2),r=n(0),i=n(6),s=n(1);e.exports=function(e,t,n){n=void 0===n?function(){}:n,Object.defineProperty(e,t,{get:function e(){i()||r(this,"lockSsfi")||r(this,"ssfi",e);var t=n.call(this);if(void 0!==t)return t;var a=new o.Assertion;return s(this,a),a},configurable:!0})}},function(e,t,n){"use strict"; 299 | /*! 300 | * deep-eql 301 | * Copyright(c) 2013 Jake Luer 302 | * MIT Licensed 303 | */var o=n(8);function r(){this._key="chai/deep-eql__"+Math.random()+Date.now()}r.prototype={get:function(e){return e[this._key]},set:function(e,t){Object.isExtensible(e)&&Object.defineProperty(e,this._key,{value:t,configurable:!0})}};var i="function"==typeof WeakMap?WeakMap:r; 304 | /*! 305 | * Check to see if the MemoizeMap has recorded a result of the two operands 306 | * 307 | * @param {Mixed} leftHandOperand 308 | * @param {Mixed} rightHandOperand 309 | * @param {MemoizeMap} memoizeMap 310 | * @returns {Boolean|null} result 311 | */function s(e,t,n){if(!n||b(e)||b(t))return null;var o=n.get(e);if(o){var r=o.get(t);if("boolean"==typeof r)return r}return null} 312 | /*! 313 | * Set the result of the equality into the MemoizeMap 314 | * 315 | * @param {Mixed} leftHandOperand 316 | * @param {Mixed} rightHandOperand 317 | * @param {MemoizeMap} memoizeMap 318 | * @param {Boolean} result 319 | */function a(e,t,n,o){if(n&&!b(e)&&!b(t)){var r=n.get(e);r?r.set(t,o):((r=new i).set(t,o),n.set(e,r))}} 320 | /*! 321 | * Primary Export 322 | */function c(e,t,n){if(n&&n.comparator)return f(e,t,n);var o=u(e,t);return null!==o?o:f(e,t,n)}function u(e,t){return e===t?0!==e||1/e==1/t:e!=e&&t!=t||!b(e)&&!b(t)&&null} 323 | /*! 324 | * The main logic of the `deepEqual` function. 325 | * 326 | * @param {Mixed} leftHandOperand 327 | * @param {Mixed} rightHandOperand 328 | * @param {Object} [options] (optional) Additional options 329 | * @param {Array} [options.comparator] (optional) Override default algorithm, determining custom equality. 330 | * @param {Array} [options.memoize] (optional) Provide a custom memoization object which will cache the results of 331 | complex objects for a speed boost. By passing `false` you can disable memoization, but this will cause circular 332 | references to blow the stack. 333 | * @return {Boolean} equal match 334 | */function f(e,t,n){(n=n||{}).memoize=!1!==n.memoize&&(n.memoize||new i);var r=n&&n.comparator,f=s(e,t,n.memoize);if(null!==f)return f;var b=s(t,e,n.memoize);if(null!==b)return b;if(r){var m=r(e,t);if(!1===m||!0===m)return a(e,t,n.memoize,m),m;var w=u(e,t);if(null!==w)return w}var g=o(e);if(g!==o(t))return a(e,t,n.memoize,!1),!1;a(e,t,n.memoize,!0);var v=function(e,t,n,o){switch(n){case"String":case"Number":case"Boolean":case"Date":return c(e.valueOf(),t.valueOf());case"Promise":case"Symbol":case"function":case"WeakMap":case"WeakSet":case"Error":return e===t;case"Arguments":case"Int8Array":case"Uint8Array":case"Uint8ClampedArray":case"Int16Array":case"Uint16Array":case"Int32Array":case"Uint32Array":case"Float32Array":case"Float64Array":case"Array":return d(e,t,o);case"RegExp": 335 | /*! 336 | * Compare two Regular Expressions for equality. 337 | * 338 | * @param {RegExp} leftHandOperand 339 | * @param {RegExp} rightHandOperand 340 | * @return {Boolean} result 341 | */ 342 | return function(e,t){return e.toString()===t.toString()} 343 | /*! 344 | * Compare two Sets/Maps for equality. Faster than other equality functions. 345 | * 346 | * @param {Set} leftHandOperand 347 | * @param {Set} rightHandOperand 348 | * @param {Object} [options] (Optional) 349 | * @return {Boolean} result 350 | */(e,t);case"Generator": 351 | /*! 352 | * Simple equality for generator objects such as those returned by generator functions. 353 | * 354 | * @param {Iterable} leftHandOperand 355 | * @param {Iterable} rightHandOperand 356 | * @param {Object} [options] (Optional) 357 | * @return {Boolean} result 358 | */ 359 | return function(e,t,n){return d(h(e),h(t),n)} 360 | /*! 361 | * Determine if the given object has an @@iterator function. 362 | * 363 | * @param {Object} target 364 | * @return {Boolean} `true` if the object has an @@iterator function. 365 | */(e,t,o);case"DataView":return d(new Uint8Array(e.buffer),new Uint8Array(t.buffer),o);case"ArrayBuffer":return d(new Uint8Array(e),new Uint8Array(t),o);case"Set":case"Map":return p(e,t,o);default: 366 | /*! 367 | * Recursively check the equality of two Objects. Once basic sameness has been established it will defer to `deepEqual` 368 | * for each enumerable key in the object. 369 | * 370 | * @param {Mixed} leftHandOperand 371 | * @param {Mixed} rightHandOperand 372 | * @param {Object} [options] (Optional) 373 | * @return {Boolean} result 374 | */ 375 | return function(e,t,n){var o=y(e),r=y(t);if(o.length&&o.length===r.length)return o.sort(),r.sort(),!1!==d(o,r)&& 376 | /*! 377 | * Determines if two objects have matching values, given a set of keys. Defers to deepEqual for the equality check of 378 | * each key. If any value of the given key is not equal, the function will return false (early). 379 | * 380 | * @param {Mixed} leftHandOperand 381 | * @param {Mixed} rightHandOperand 382 | * @param {Array} keys An array of keys to compare the values of leftHandOperand and rightHandOperand against 383 | * @param {Object} [options] (Optional) 384 | * @return {Boolean} result 385 | */ 386 | function(e,t,n,o){var r=n.length;if(0===r)return!0;for(var i=0;i 427 | * MIT Licensed 428 | */ 429 | e.exports=function(e){var t=[];for(var n in e)t.push(n);return t}},function(e,t,n){ 430 | /*! 431 | * Chai - message composition utility 432 | * Copyright(c) 2012-2014 Jake Luer 433 | * MIT Licensed 434 | */ 435 | /*! 436 | * Module dependencies 437 | */ 438 | var o=n(0),r=n(13),i=n(12);e.exports=function(e,t){var n=o(e,"negate"),s=o(e,"object"),a=t[3],c=r(e,t),u=n?t[2]:t[1],f=o(e,"message");return"function"==typeof u&&(u=u()),u=(u=u||"").replace(/#\{this\}/g,function(){return i(s)}).replace(/#\{act\}/g,function(){return i(c)}).replace(/#\{exp\}/g,function(){return i(a)}),f?f+": "+u:u}},function(e,t,n){ 439 | /*! 440 | * Chai - expectTypes utility 441 | * Copyright(c) 2012-2014 Jake Luer 442 | * MIT Licensed 443 | */ 444 | var o=n(14),r=n(0),i=n(8);e.exports=function(e,t){var n=r(e,"message"),s=r(e,"ssfi");n=n?n+": ":"",e=r(e,"object"),(t=t.map(function(e){return e.toLowerCase()})).sort();var a=t.map(function(e,n){var o=~["a","e","i","o","u"].indexOf(e.charAt(0))?"an":"a";return(t.length>1&&n===t.length-1?"or ":"")+o+" "+e}).join(", "),c=i(e).toLowerCase();if(!t.some(function(e){return c===e}))throw new o(n+"object tested must be "+a+", but "+c+" given",void 0,s)}},function(e,t){var n;n=function(){return this}();try{n=n||Function("return this")()||(0,eval)("this")}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t,n){ 445 | /*! 446 | * Chai - test utility 447 | * Copyright(c) 2012-2014 Jake Luer 448 | * MIT Licensed 449 | */ 450 | /*! 451 | * Module dependencies 452 | */ 453 | var o=n(0);e.exports=function(e,t){var n=o(e,"negate"),r=t[0];return n?!r:r}},function(e,t,n){"use strict";function o(e,t){return void 0!==e&&null!==e&&t in Object(e)}function r(e){return e.replace(/([^\\])\[/g,"$1.[").match(/(\\\.|[^.]+?)+/g).map(function(e){var t=/^\[(\d+)\]$/.exec(e);return t?{i:parseFloat(t[1])}:{p:e.replace(/\\([.\[\]])/g,"$1")}})}function i(e,t,n){var o=e,r=null;n=void 0===n?t.length:n;for(var i=0;i1?i(e,n,n.length-1):e,name:s.p||s.i,value:i(e,n)};return a.exists=o(a.parent,a.name),a}e.exports={hasProperty:o,getPathInfo:s,getPathValue:function(e,t){return s(e,t).value},setPathValue:function(e,t,n){return function(e,t,n){for(var o=e,r=n.length,i=null,s=0;s 457 | * MIT Licensed 458 | */ 459 | /*! 460 | * Dependencies that are used for multiple exports are required here only once 461 | */ 462 | var o=n(37); 463 | /*! 464 | * test utility 465 | */t.test=n(36), 466 | /*! 467 | * type utility 468 | */ 469 | t.type=n(8), 470 | /*! 471 | * expectTypes utility 472 | */ 473 | t.expectTypes=n(34), 474 | /*! 475 | * message utility 476 | */ 477 | t.getMessage=n(33), 478 | /*! 479 | * actual utility 480 | */ 481 | t.getActual=n(13), 482 | /*! 483 | * Inspect util 484 | */ 485 | t.inspect=n(7), 486 | /*! 487 | * Object Display util 488 | */ 489 | t.objDisplay=n(12), 490 | /*! 491 | * Flag utility 492 | */ 493 | t.flag=n(0), 494 | /*! 495 | * Flag transferring utility 496 | */ 497 | t.transferFlags=n(1), 498 | /*! 499 | * Deep equal utility 500 | */ 501 | t.eql=n(31), 502 | /*! 503 | * Deep path info 504 | */ 505 | t.getPathInfo=o.getPathInfo, 506 | /*! 507 | * Check if a property exists 508 | */ 509 | t.hasProperty=o.hasProperty, 510 | /*! 511 | * Function name 512 | */ 513 | t.getName=n(11), 514 | /*! 515 | * add Property 516 | */ 517 | t.addProperty=n(30), 518 | /*! 519 | * add Method 520 | */ 521 | t.addMethod=n(29), 522 | /*! 523 | * overwrite Property 524 | */ 525 | t.overwriteProperty=n(28), 526 | /*! 527 | * overwrite Method 528 | */ 529 | t.overwriteMethod=n(27), 530 | /*! 531 | * Add a chainable method 532 | */ 533 | t.addChainableMethod=n(26), 534 | /*! 535 | * Overwrite chainable method 536 | */ 537 | t.overwriteChainableMethod=n(25), 538 | /*! 539 | * Compare by inspect method 540 | */ 541 | t.compareByInspect=n(24), 542 | /*! 543 | * Get own enumerable property symbols method 544 | */ 545 | t.getOwnEnumerablePropertySymbols=n(9), 546 | /*! 547 | * Get own enumerable properties method 548 | */ 549 | t.getOwnEnumerableProperties=n(23), 550 | /*! 551 | * Checks error against a given set of criteria 552 | */ 553 | t.checkError=n(22), 554 | /*! 555 | * Proxify util 556 | */ 557 | t.proxify=n(4), 558 | /*! 559 | * addLengthGuard util 560 | */ 561 | t.addLengthGuard=n(5), 562 | /*! 563 | * isProxyEnabled helper 564 | */ 565 | t.isProxyEnabled=n(6), 566 | /*! 567 | * isNaN method 568 | */ 569 | t.isNaN=n(21)},function(e,t,n){e.exports=n(2)},function(e,t,n){"use strict";var o=n(39);function r(e){return document.querySelector("#root").innerHTML=e}describe("Presto",function(){afterEach(function(){r("")}),describe("Component",function(){describe("scan",function(){it("returns a Map",function(){var e=Presto.Component.scan();o.assert.typeOf(e,"Map"),o.assert.equal(e.size,0)}),it("finds a component",function(){r('\n
\n
\n Counter is: 1\n
\n
\n ');var e=Presto.Component.scan();o.assert.equal(e.size,1),o.assert.deepEqual(Array.from(e.keys()),["cA"]);var t=document.querySelector(".presto-component-instance#iA");(0,o.assert)(e.get("cA").has(t))}),it("finds several components",function(){r('\n
\n
\n Counter is: 1\n
\n
\n
\n
\n Counter is: 1\n
\n
\n
\n
\n Counter is: 1\n
\n
\n ');var e=Presto.Component.scan();o.assert.equal(e.size,3),o.assert.deepEqual(Array.from(e.keys()),["cA","cB","cC"]);var t=document.querySelector(".presto-component-instance#iA"),n=document.querySelector(".presto-component-instance#iB"),i=document.querySelector(".presto-component-instance#iC");(0,o.assert)(e.get("cA").has(t)),(0,o.assert)(e.get("cB").has(n)),(0,o.assert)(e.get("cC").has(i))})})})})}])}); -------------------------------------------------------------------------------- /test/presto/component_supervisor_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Presto.ComponentSupervisorTest do 2 | use ExUnit.Case, async: false 3 | alias Presto.ComponentSupervisor 4 | 5 | setup do 6 | start_supervised({Registry, keys: :unique, name: Presto.ComponentRegistry}) 7 | :ok 8 | end 9 | 10 | defmodule CounterComponent do 11 | use Presto.Component 12 | end 13 | 14 | test "fails to start two supervisors" do 15 | {:ok, _} = start_supervised(ComponentSupervisor) 16 | {:error, _} = start_supervised(ComponentSupervisor) 17 | end 18 | 19 | test "starts a component" do 20 | {:ok, _} = start_supervised(ComponentSupervisor) 21 | {:ok, _component} = ComponentSupervisor.start_component(CounterComponent, 1) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/presto/component_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Presto.ComponentTestMacros do 2 | end 3 | 4 | defmodule Presto.ComponentTest do 5 | use ExUnit.Case, async: false 6 | require Presto.ComponentTestMacros 7 | alias Presto.Component 8 | 9 | setup do 10 | start_supervised({Registry, keys: :unique, name: Presto.ComponentRegistry}) 11 | :ok 12 | end 13 | 14 | def make_response(content, component_id \\ "visitor1") do 15 | %Presto.Action.UpdateComponent{ 16 | component_id: component_id, 17 | content: "
#{content}
", 18 | name: "update_component" 19 | } 20 | end 21 | 22 | defmodule ComponentIdFixture do 23 | use Presto.Component 24 | end 25 | 26 | defmodule ComponentIdTooFixture do 27 | use Presto.Component 28 | end 29 | 30 | defmodule ComponentIdOverriddenFixture do 31 | use Presto.Component 32 | def component_id(_conn), do: 7 33 | end 34 | 35 | describe "component_id/1" do 36 | test "starts with a component_id" do 37 | {:ok, _pid} = Component.start_link(ComponentIdFixture, "visitor1") 38 | end 39 | 40 | test "errors on duplicate component_id" do 41 | {:ok, _pid} = Component.start_link(ComponentIdFixture, "visitor1") 42 | {:error, {:already_started, _pid}} = Component.start_link(ComponentIdFixture, "visitor1") 43 | end 44 | 45 | test "is overridable" do 46 | assert 7 == ComponentIdOverriddenFixture.component_id(nil) 47 | end 48 | end 49 | 50 | defmodule StartLinkFixture do 51 | use Presto.Component 52 | end 53 | 54 | describe "start_link/3" do 55 | test "can give a different initial model" do 56 | {:ok, pid1} = Component.start_link(StartLinkFixture, "visitor1", 1) 57 | {:ok, pid2} = Component.start_link(StartLinkFixture, "visitor2", 2) 58 | 59 | {:ok, res1} = Presto.Component.update(pid1, :current) 60 | {:ok, res2} = Presto.Component.update(pid2, :current) 61 | 62 | assert res1 == make_response("1") 63 | assert res2 == make_response("2", "visitor2") 64 | end 65 | end 66 | 67 | defmodule InitialModelFixture do 68 | use Presto.Component 69 | def initial_model(model), do: model + 3 70 | end 71 | 72 | describe "initial_model/1" do 73 | test "can override the initial model" do 74 | {:ok, pid} = Component.start_link(InitialModelFixture, "visitor1", 1) 75 | 76 | {:ok, res} = Presto.Component.update(pid, :current) 77 | 78 | assert res == make_response("4") 79 | end 80 | end 81 | 82 | defmodule UpdateFixture do 83 | use Presto.Component 84 | 85 | def update(message, model) do 86 | case message do 87 | :current -> model 88 | :inc -> model + 1 89 | end 90 | end 91 | end 92 | 93 | describe "update/2" do 94 | test "performs an update" do 95 | {:ok, pid} = Component.start_link(UpdateFixture, "visitor1", 1) 96 | 97 | {:ok, result1} = Presto.Component.update(pid, :current) 98 | {:ok, result2} = Presto.Component.update(pid, :inc) 99 | 100 | assert result1 == make_response("1") 101 | assert result2 == make_response("2") 102 | end 103 | end 104 | 105 | describe "render/1" do 106 | test "renders the current state" do 107 | {:ok, pid} = Component.start_link(UpdateFixture, "visitor1", 1) 108 | 109 | {:ok, result1} = Presto.Component.render(pid) 110 | 111 | assert result1 == 112 | {:safe, 113 | [ 114 | 60, 115 | "div", 116 | [ 117 | [32, "class", 61, 34, "presto-component", 34], 118 | [32, "id", 61, 34, "visitor1", 34] 119 | ], 120 | 62, 121 | "1", 122 | 60, 123 | 47, 124 | "div", 125 | 62 126 | ]} 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /test/presto_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PrestoTest do 2 | use ExUnit.Case, async: false 3 | # doctest Presto 4 | 5 | setup do 6 | start_supervised({Registry, keys: :unique, name: Presto.ComponentRegistry}) 7 | start_supervised(Presto.ComponentSupervisor) 8 | :ok 9 | end 10 | 11 | defmodule CounterComponent do 12 | use Presto.Component 13 | 14 | def update(message, model) do 15 | case message do 16 | :current -> model 17 | :inc -> model + 1 18 | end 19 | end 20 | 21 | def render(model) do 22 | {:safe, "Counter is: #{model}"} 23 | end 24 | end 25 | 26 | defmodule CounterComponent2 do 27 | use Presto.Component 28 | end 29 | 30 | describe "create_component/2" do 31 | test "creates a component" do 32 | {:ok, pid} = Presto.create_component(CounterComponent, 1) 33 | assert is_pid(pid) 34 | end 35 | 36 | test "creates several components" do 37 | {:ok, _pid} = Presto.create_component(CounterComponent, 1) 38 | {:ok, _pid} = Presto.create_component(CounterComponent, 2) 39 | {:ok, _pid} = Presto.create_component(CounterComponent, 3) 40 | end 41 | 42 | test "fails to create duplicate component" do 43 | {:ok, _pid} = Presto.create_component(CounterComponent, 123) 44 | {:error, :process_already_exists} = Presto.create_component(CounterComponent, 123) 45 | end 46 | end 47 | 48 | describe "find_component/2" do 49 | test "finds a component" do 50 | {:ok, _pid} = Presto.create_component(CounterComponent, 123) 51 | {:ok, pid} = Presto.find_component(123) 52 | assert is_pid(pid) 53 | end 54 | 55 | test "fails to find a component" do 56 | {:error, :no_such_component} = Presto.find_component(123) 57 | end 58 | end 59 | 60 | describe "find_or_create_component/2" do 61 | test "creates a component if non-existent" do 62 | {:ok, pid} = Presto.find_or_create_component(CounterComponent, 1) 63 | assert is_pid(pid) 64 | end 65 | 66 | test "returns existing component" do 67 | {:ok, pid} = Presto.create_component(CounterComponent, 1) 68 | {:ok, ^pid} = Presto.find_or_create_component(CounterComponent, 1) 69 | end 70 | end 71 | 72 | describe "component_exists?/2" do 73 | test "false if component does not exist" do 74 | refute Presto.component_exists?(123) 75 | end 76 | 77 | test "returns existing component" do 78 | {:ok, _pid} = Presto.create_component(CounterComponent, 123) 79 | assert Presto.component_exists?(123) 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------