├── .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 |
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 | //
113 | // `.trim()
114 | // let div = tag("div", {}, content)
115 |
116 | // DOM.undoRefs(1, div)
117 | // expect(div.innerHTML).toBe(`
118 | //
119 | //
125 | // `.trim())
126 |
127 | // DOM.undoRefs(38, div)
128 | // expect(div.innerHTML).toBe(`
129 | //
130 | //
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 | Click Me
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 | Click Me
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 | Click Me
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 | Click Me
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 ');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 \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 |
--------------------------------------------------------------------------------