├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── elm-package.json ├── examples ├── elm-package.json ├── package.json └── src │ ├── DocumentExample.elm │ ├── LocationExample.elm │ ├── StorageExample.elm │ └── WindowExample.elm ├── src ├── Native │ └── WebAPI │ │ ├── AnimationFrame.js │ │ ├── Date.js │ │ ├── Document.js │ │ ├── Event.js │ │ ├── Function.js │ │ ├── Listener.js │ │ ├── Location.js │ │ ├── Math.js │ │ ├── Native.js │ │ ├── Number.js │ │ ├── Screen.js │ │ ├── Storage.js │ │ └── Window.js └── WebAPI │ ├── AnimationFrame.elm │ ├── Cookie.elm │ ├── Date.elm │ ├── Document.elm │ ├── Event.elm │ ├── Event │ ├── BeforeUnload.elm │ ├── Custom.elm │ └── Internal.elm │ ├── Function.elm │ ├── Location.elm │ ├── Math.elm │ ├── Native.elm │ ├── Number.elm │ ├── Screen.elm │ ├── Storage.elm │ └── Window.elm └── test ├── build └── .keepme ├── ci.sh ├── elm-package.json ├── jshint.json ├── package.json └── src ├── config.js ├── coverage.js ├── elm ├── Browser.elm ├── CI.elm ├── DisableRAF.elm ├── DisableStorage.elm ├── Native │ ├── DisableRaf.js │ ├── EnableRaf.js │ └── TestUtil.js ├── TestMailbox.elm ├── TestUtil.elm ├── Tests.elm ├── Variant.elm └── WebAPI │ ├── AnimationFrameTest.elm │ ├── CookieTest.elm │ ├── DateTest.elm │ ├── DocumentTest.elm │ ├── Event │ └── CustomEventTest.elm │ ├── EventTest.elm │ ├── FunctionTest.elm │ ├── LocationTest.elm │ ├── MathTest.elm │ ├── NumberTest.elm │ ├── ScreenTest.elm │ ├── StorageTest.elm │ └── WindowTest.elm ├── mocha ├── browser.js ├── elmTest.js ├── locationTest.js ├── storageTest.js └── windowTest.js ├── remote.js ├── run.js └── selenium-sauce.js /.gitignore: -------------------------------------------------------------------------------- 1 | **/elm-stuff 2 | **/elm.js 3 | **/index.html 4 | **/elm.html 5 | **/node_modules 6 | **/npm-debug.log 7 | **/elmclient.log 8 | test/build 9 | examples/*.html 10 | .*.swp 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 0.12 5 | 6 | sudo: false 7 | 8 | install: 9 | - cd test 10 | - npm install 11 | - ./node_modules/.bin/elm-package install --yes || ./node_modules/.bin/elm-package install --yes 12 | 13 | script: 14 | - npm run ci 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Ryan Rempel 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.1.0", 3 | "summary": "Expose Web APIs provided by the browser's Javascript runtime", 4 | "repository": "https://github.com/rgrempel/elm-web-api.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "src" 8 | ], 9 | "exposed-modules": [ 10 | "WebAPI.AnimationFrame", 11 | "WebAPI.Cookie", 12 | "WebAPI.Document", 13 | "WebAPI.Date", 14 | "WebAPI.Event", 15 | "WebAPI.Event.BeforeUnload", 16 | "WebAPI.Event.Custom", 17 | "WebAPI.Function", 18 | "WebAPI.Location", 19 | "WebAPI.Math", 20 | "WebAPI.Number", 21 | "WebAPI.Screen", 22 | "WebAPI.Storage", 23 | "WebAPI.Window" 24 | ], 25 | "native-modules": true, 26 | "dependencies": { 27 | "elm-lang/core": "3.0.0 <= v < 4.0.0" 28 | }, 29 | "elm-version": "0.16.0 <= v < 0.17.0" 30 | } 31 | -------------------------------------------------------------------------------- /examples/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Examples for the WebAPI module", 4 | "repository": "https://github.com/rgrempel/elm-web-api.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "src", 8 | "../src" 9 | ], 10 | "exposed-modules": [], 11 | "native-modules": true, 12 | "dependencies": { 13 | "elm-lang/core": "3.0.0 <= v < 4.0.0", 14 | "evancz/elm-effects": "2.0.1 <= v < 3.0.0", 15 | "evancz/elm-html": "4.0.2 <= v < 5.0.0", 16 | "evancz/start-app": "2.0.2 <= v < 3.0.0" 17 | }, 18 | "elm-version": "0.16.0 <= v < 0.17.0" 19 | } -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elm-web-api", 3 | "version": "1.0.0", 4 | "description": "Tests for elm-web-api", 5 | "main": "elm.js", 6 | "dependencies": { 7 | "elm": "0.16.0" 8 | }, 9 | "devDependencies": {}, 10 | "scripts": { 11 | "test": "elm-make src/WindowExample.elm --output elm.html && open elm.html;", 12 | "package": "elm-package install;" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/rgrempel/elm-web-api.git" 17 | }, 18 | "author": "Ryan Rempel ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/rgrempel/elm-web-api/issues" 22 | }, 23 | "homepage": "https://github.com/rgrempel/elm-web-api#readme" 24 | } 25 | -------------------------------------------------------------------------------- /examples/src/DocumentExample.elm: -------------------------------------------------------------------------------- 1 | module DocumentExample where 2 | 3 | import Effects exposing (Effects, Never) 4 | import StartApp exposing (App) 5 | import Task exposing (Task, toResult) 6 | import Html exposing (Html, h4, div, text, button, p, input) 7 | import Html.Attributes exposing (id, style) 8 | import Html.Events exposing (onClick) 9 | import Signal exposing (Signal, Mailbox, Address) 10 | 11 | import WebAPI.Event exposing 12 | ( Listener, removeListener, on, once, select 13 | , remove, send, performTask, preventDefault 14 | , stopPropagation, stopImmediatePropagation 15 | ) 16 | 17 | import WebAPI.Document exposing 18 | ( getReadyState, ReadyState(..) 19 | , domContentLoaded, loaded, target 20 | ) 21 | 22 | 23 | app : App Model 24 | app = 25 | StartApp.start 26 | { init = init 27 | , update = update 28 | , view = view 29 | , inputs = [ mailbox.signal ] 30 | } 31 | 32 | 33 | mailbox : Mailbox Action 34 | mailbox = Signal.mailbox NoOp 35 | 36 | 37 | main : Signal Html 38 | main = app.html 39 | 40 | 41 | port tasks : Signal (Task.Task Never ()) 42 | port tasks = app.tasks 43 | 44 | 45 | type alias Model = 46 | { log : List Log 47 | , clickListener : Maybe Listener 48 | } 49 | 50 | 51 | type Log 52 | = DomContentLoadedTwice ReadyState 53 | | LoadedTwice ReadyState 54 | | ListeningForOneClick 55 | | GotOneClick 56 | | ListeningForKeys Bool 57 | | RespondedViaMessage 58 | | RespondedViaTask 59 | | TestingRemove 60 | 61 | 62 | type Action 63 | = NoOp 64 | | Write Log 65 | | ListenForOneClick 66 | | ListenForKeys Bool 67 | | SetClickListener (Maybe Listener) 68 | | TestRemove 69 | 70 | 71 | init : (Model, Effects Action) 72 | init = 73 | let 74 | action1 = 75 | Effects.task <| 76 | Task.map 77 | (Write << DomContentLoadedTwice) 78 | getReadyState `Task.andThen` (\state -> 79 | Task.map (always state) <| 80 | domContentLoaded `Task.andThen` 81 | always domContentLoaded 82 | ) 83 | 84 | action2 = 85 | Effects.task <| 86 | Task.map 87 | (Write << LoadedTwice) 88 | getReadyState `Task.andThen` (\state -> 89 | Task.map (always state) <| 90 | loaded `Task.andThen` 91 | always loaded 92 | ) 93 | in 94 | ( Model [] Nothing 95 | , Effects.batch 96 | [ action1 97 | , action2 98 | ] 99 | ) 100 | 101 | 102 | update : Action -> Model -> (Model, Effects Action) 103 | update action model = 104 | case action of 105 | NoOp -> 106 | ( model, Effects.none ) 107 | 108 | Write entry -> 109 | ( { model | log = entry :: model.log } 110 | , Effects.none 111 | ) 112 | 113 | ListenForOneClick -> 114 | ( { model | log = ListeningForOneClick :: model.log } 115 | , Effects.task <| 116 | Task.map 117 | (always (Write GotOneClick)) 118 | (once (select "click") target) 119 | ) 120 | 121 | TestRemove -> 122 | ( { model | log = TestingRemove :: model.log } 123 | , Effects.task <| 124 | Task.map (always NoOp) <| 125 | on (select "click") (\event listener -> 126 | [ remove 127 | , performTask <| 128 | Signal.send mailbox.address <| 129 | Write RespondedViaTask 130 | ] 131 | ) target 132 | ) 133 | 134 | ListenForKeys start -> 135 | case (start, model.clickListener) of 136 | (True, Nothing) -> 137 | ( { model | log = ListeningForKeys True :: model.log } 138 | , Effects.task <| 139 | Task.map 140 | (SetClickListener << Just) <| 141 | on (select "keypress") (\event listener -> 142 | [ stopPropagation event 143 | , stopImmediatePropagation event 144 | , preventDefault event 145 | , performTask <| 146 | Signal.send mailbox.address <| 147 | Write RespondedViaTask 148 | 149 | , send <| 150 | Signal.message mailbox.address <| 151 | Write RespondedViaMessage 152 | ] 153 | ) target 154 | ) 155 | 156 | (False, Just listener) -> 157 | ( { model | log = ListeningForKeys False :: model.log } 158 | , Effects.task <| 159 | Task.map 160 | (always (SetClickListener Nothing)) 161 | (removeListener listener) 162 | ) 163 | 164 | (_, _) -> 165 | ( model, Effects.none ) 166 | 167 | SetClickListener listener -> 168 | ( { model | clickListener = listener } 169 | , Effects.none 170 | ) 171 | 172 | 173 | view : Address Action -> Model -> Html 174 | view address model = 175 | div 176 | [ style 177 | [ ("padding", "8px") 178 | ] 179 | ] 180 | [ button 181 | [ onClick address ListenForOneClick 182 | , id "listen-for-one-click" 183 | ] 184 | [ text "Listen for one click" ] 185 | , button 186 | [ onClick address (ListenForKeys True) 187 | , id "listen-for-keys-true" 188 | ] 189 | [ text "Listen for keyup" ] 190 | , button 191 | [ onClick address (ListenForKeys False) 192 | , id "listen-for-keys-false" 193 | ] 194 | [ text "Stop listening for keyup" ] 195 | , button 196 | [ onClick address TestRemove 197 | , id "test-remove-response" 198 | ] 199 | [ text "Listen for click and remove" ] 200 | , div [] 201 | [ input [] [] 202 | ] 203 | , h4 [] [ text "Log (most recent first)" ] 204 | , div 205 | [ id "log" ] 206 | <| 207 | List.map (\entry -> 208 | p [] [ text (toString entry) ] 209 | ) model.log 210 | ] 211 | -------------------------------------------------------------------------------- /examples/src/LocationExample.elm: -------------------------------------------------------------------------------- 1 | module LocationExample where 2 | 3 | import Effects exposing (Effects, Never) 4 | import StartApp exposing (App) 5 | import Task exposing (Task, toResult) 6 | import Html exposing (Html, h4, div, text, button, input) 7 | import Html.Attributes exposing (id, type') 8 | import Html.Events exposing (onClick, targetValue, on) 9 | import Signal exposing (Signal, Address) 10 | 11 | import WebAPI.Location exposing (reload, assign, replace, Source(..)) 12 | 13 | 14 | app : App Model 15 | app = 16 | StartApp.start 17 | { init = init 18 | , update = update 19 | , view = view 20 | , inputs = [] 21 | } 22 | 23 | 24 | main : Signal Html 25 | main = app.html 26 | 27 | 28 | port tasks : Signal (Task.Task Never ()) 29 | port tasks = app.tasks 30 | 31 | 32 | type alias Model = 33 | { message : String 34 | , url : String 35 | } 36 | 37 | 38 | init : (Model, Effects Action) 39 | init = (Model "Initial state" "", Effects.none) 40 | 41 | 42 | type Action 43 | = Reload Source 44 | | HandleReload (Result String ()) 45 | | DoAssign String 46 | | HandleAssign (Result String ()) 47 | | DoReplace String 48 | | HandleReplace (Result String ()) 49 | | SetUrl String 50 | 51 | 52 | update : Action -> Model -> (Model, Effects Action) 53 | update action model = 54 | case action of 55 | HandleReload result -> 56 | ( { model | message = "Reloaded (but if this stays, then that's an error)" } 57 | , Effects.none 58 | ) 59 | 60 | Reload source -> 61 | ( { model | message = "About to reload" } 62 | , reload source |> 63 | toResult |> 64 | Task.map HandleReload |> 65 | Effects.task 66 | ) 67 | 68 | SetUrl url -> 69 | ( { model | url = url } 70 | , Effects.none 71 | ) 72 | 73 | HandleAssign result -> 74 | let 75 | message = 76 | case result of 77 | Ok _ -> 78 | "Assigned (but if this stays, then that's an error)" 79 | 80 | Err err -> 81 | "Got error: " ++ err 82 | in 83 | ( { model | message = message } 84 | , Effects.none 85 | ) 86 | 87 | DoAssign url -> 88 | ( { model | message = "About to assign" } 89 | , assign url |> 90 | toResult |> 91 | Task.map HandleAssign |> 92 | Effects.task 93 | ) 94 | 95 | HandleReplace result -> 96 | let 97 | message = 98 | case result of 99 | Ok _ -> 100 | "Replaced (but if this stays, then that's an error)" 101 | 102 | Err err -> 103 | "Got error: " ++ err 104 | in 105 | ( { model | message = message } 106 | , Effects.none 107 | ) 108 | 109 | DoReplace url -> 110 | ( { model | message = "About to replace" } 111 | , replace url |> 112 | toResult |> 113 | Task.map HandleReplace |> 114 | Effects.task 115 | ) 116 | 117 | 118 | view : Address Action -> Model -> Html 119 | view address model = 120 | div [] 121 | [ button 122 | [ id "reload-force-button" 123 | , onClick address (Reload ForceServer) 124 | ] 125 | [ text "WebAPI.Location.reload ForceServer" ] 126 | , button 127 | [ id "reload-cache-button" 128 | , onClick address (Reload AllowCache) 129 | ] 130 | [ text "WebAPI.Location.reload AllowCache" ] 131 | , div [] 132 | [ button 133 | [ id "assign-button" 134 | , onClick address (DoAssign model.url) 135 | ] 136 | [ text "Location.assign" ] 137 | , button 138 | [ id "replace-button" 139 | , onClick address (DoReplace model.url) 140 | ] 141 | [ text "Location.replace" ] 142 | , input 143 | [ id "input" 144 | , type' "text" 145 | , on "input" targetValue (Signal.message address << SetUrl) 146 | ] [] 147 | ] 148 | 149 | 150 | , h4 [] [ text "Message" ] 151 | , div [ id "message" ] [ text model.message ] 152 | ] 153 | 154 | -------------------------------------------------------------------------------- /examples/src/WindowExample.elm: -------------------------------------------------------------------------------- 1 | module WindowExample where 2 | 3 | import Effects exposing (Effects, Never) 4 | import StartApp exposing (App) 5 | import Task exposing (Task, toResult) 6 | import Html exposing (Html, h4, div, text, button) 7 | import Html.Attributes exposing (id) 8 | import Html.Events exposing (onClick) 9 | import Signal exposing (Signal, Address) 10 | 11 | import WebAPI.Event exposing (Listener, removeListener) 12 | import WebAPI.Window exposing (alert, confirm, prompt, isOnline, confirmUnload) 13 | import WebAPI.Event.BeforeUnload exposing (BeforeUnloadEvent) 14 | 15 | 16 | app : App Model 17 | app = 18 | StartApp.start 19 | { init = init 20 | , update = update 21 | , view = view 22 | , inputs = 23 | [ Signal.map HandleOnlineSignal WebAPI.Window.online 24 | ] 25 | } 26 | 27 | 28 | main : Signal Html 29 | main = app.html 30 | 31 | 32 | port tasks : Signal (Task.Task Never ()) 33 | port tasks = app.tasks 34 | 35 | 36 | type alias Model = 37 | { message : String 38 | , confirmUnloadListener : Maybe Listener 39 | } 40 | 41 | 42 | init : (Model, Effects Action) 43 | init = (Model "" Nothing, Effects.none) 44 | 45 | 46 | type Action 47 | = ShowAlert String 48 | | HandleAlertResponse 49 | | ShowConfirm String 50 | | HandleConfirmResponse (Result () ()) 51 | | ShowPrompt String String 52 | | HandlePromptResponse (Result () String) 53 | | CheckOnline 54 | | HandleOnlineResponse (Result () Bool) 55 | | HandleOnlineSignal Bool 56 | | ConfirmUnload Bool 57 | | SetConfirmUnloadListener (Maybe Listener) 58 | 59 | 60 | update : Action -> Model -> (Model, Effects Action) 61 | update action model = 62 | case action of 63 | ShowAlert message -> 64 | ( model 65 | , alert message |> 66 | Task.map (always HandleAlertResponse) |> 67 | Effects.task 68 | ) 69 | 70 | HandleAlertResponse -> 71 | ( { model | message = "Got alert response" } 72 | , Effects.none 73 | ) 74 | 75 | ShowConfirm message -> 76 | ( model 77 | , confirm message |> 78 | toResult |> 79 | Task.map HandleConfirmResponse |> 80 | Effects.task 81 | ) 82 | 83 | HandleConfirmResponse result -> 84 | let 85 | message = 86 | case result of 87 | Ok _ -> "Pressed OK" 88 | Err _ -> "Pressed cancel" 89 | in 90 | ( { model | message = message } 91 | , Effects.none 92 | ) 93 | 94 | ShowPrompt message default -> 95 | ( model 96 | , prompt message default |> 97 | toResult |> 98 | Task.map HandlePromptResponse |> 99 | Effects.task 100 | ) 101 | 102 | HandlePromptResponse result -> 103 | let 104 | message = 105 | case result of 106 | Ok response -> "Got response: " ++ response 107 | Err _ -> "User canceled." 108 | 109 | in 110 | ( { model | message = message } 111 | , Effects.none 112 | ) 113 | 114 | CheckOnline -> 115 | ( model 116 | , isOnline |> 117 | toResult |> 118 | Task.map HandleOnlineResponse |> 119 | Effects.task 120 | ) 121 | 122 | HandleOnlineResponse result -> 123 | let 124 | message = 125 | case result of 126 | Ok online -> 127 | "Am I online? " ++ (toString online) 128 | 129 | Err _ -> 130 | "Got err ... shouldn't happen" 131 | 132 | in 133 | ( { model | message = message } 134 | , Effects.none 135 | ) 136 | 137 | HandleOnlineSignal online -> 138 | ( { model | 139 | message = 140 | if online 141 | then "I'm online now" 142 | else "I'm offline now" 143 | } 144 | , Effects.none 145 | ) 146 | 147 | ConfirmUnload enable -> 148 | ( model 149 | , case (enable, model.confirmUnloadListener) of 150 | (True, Nothing) -> 151 | Effects.task <| 152 | Task.map 153 | (SetConfirmUnloadListener << Just) 154 | (confirmUnload "Are you sure you want to leave?") 155 | 156 | (False, Just listener) -> 157 | Effects.task <| 158 | Task.map 159 | (always (SetConfirmUnloadListener Nothing)) 160 | (removeListener listener) 161 | 162 | (_, _) -> 163 | Effects.none 164 | ) 165 | 166 | SetConfirmUnloadListener listener -> 167 | ( { model | confirmUnloadListener = listener } 168 | , Effects.none 169 | ) 170 | 171 | 172 | view : Address Action -> Model -> Html 173 | view address model = 174 | div [] 175 | [ button 176 | [ onClick address (ShowAlert "Hello world!") 177 | , id "alert-button" 178 | ] 179 | [ text "WebAPI.Window.alert" ] 180 | , button 181 | [ onClick address (ShowConfirm "Do you agree?") 182 | , id "confirm-button" 183 | ] 184 | [ text "WebAPI.Window.confirm" ] 185 | , button 186 | [ onClick address (ShowPrompt "What is your favourite colour?" "Blue") 187 | , id "prompt-button" 188 | ] 189 | [ text "WebAPI.Window.prompt" ] 190 | , button 191 | [ onClick address CheckOnline 192 | , id "online-button" 193 | ] 194 | [ text "WebAPI.Window.isOnline" ] 195 | , button 196 | [ onClick address (ConfirmUnload True) 197 | , id "enable-confirm-unload" 198 | ] 199 | [ text "Enable confirmUnload" ] 200 | , button 201 | [ onClick address (ConfirmUnload False) 202 | , id "disable-confirm-unload" 203 | ] 204 | [ text "Disable confirmUnload" ] 205 | , h4 [] [ text "Message" ] 206 | , div [ id "message" ] [ text model.message ] 207 | ] 208 | 209 | -------------------------------------------------------------------------------- /src/Native/WebAPI/AnimationFrame.js: -------------------------------------------------------------------------------- 1 | Elm.Native = Elm.Native || {}; 2 | Elm.Native.WebAPI = Elm.Native.WebAPI || {}; 3 | Elm.Native.WebAPI.AnimationFrame = Elm.Native.WebAPI.AnimationFrame || {}; 4 | 5 | // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ 6 | // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating 7 | 8 | // requestAnimationFrame polyfill by Erik Möller 9 | // fixes from Paul Irish and Tino Zijdel 10 | // list-based fallback implementation by Jonas Finnemann Jensen 11 | 12 | var raf = window.requestAnimationFrame; 13 | var caf = window.cancelAnimationFrame; 14 | 15 | if (!raf) { 16 | var tid = null, cbs = [], nb = 0, ts = 0; 17 | 18 | var animate = function animate () { 19 | var i, clist = cbs, len = cbs.length; 20 | tid = null; 21 | ts = Date.now(); 22 | cbs = []; 23 | nb += clist.length; 24 | 25 | for (i = 0; i < len; i++) { 26 | if (clist[i]) clist[i](ts); 27 | } 28 | }; 29 | 30 | raf = function (cb) { 31 | if (tid === null) { 32 | tid = setTimeout(animate, Math.max(0, 20 + ts - Date.now())); 33 | } 34 | 35 | return cbs.push(cb) + nb; 36 | }; 37 | 38 | caf = function (id) { 39 | delete cbs[id - nb - 1]; 40 | }; 41 | } 42 | 43 | Elm.Native.WebAPI.AnimationFrame.make = function (localRuntime) { 44 | localRuntime.Native = localRuntime.Native || {}; 45 | localRuntime.Native.WebAPI = localRuntime.Native.WebAPI || {}; 46 | localRuntime.Native.WebAPI.AnimationFrame = localRuntime.Native.WebAPI.AnimationFrame || {}; 47 | 48 | if (!localRuntime.Native.WebAPI.AnimationFrame.values) { 49 | var Task = Elm.Native.Task.make(localRuntime); 50 | var Utils = Elm.Native.Utils.make(localRuntime); 51 | 52 | localRuntime.Native.WebAPI.AnimationFrame.values = { 53 | task: Task.asyncFunction(function (callback) { 54 | raf(function (time) { 55 | callback(Task.succeed(time)); 56 | }); 57 | }), 58 | 59 | request: function (taskProducer) { 60 | return Task.asyncFunction(function (callback) { 61 | var request = raf(function (time) { 62 | Task.perform(taskProducer(time)); 63 | }); 64 | 65 | callback(Task.succeed(request)); 66 | }); 67 | }, 68 | 69 | cancel: function (request) { 70 | return Task.asyncFunction(function (callback) { 71 | caf(request); 72 | callback(Task.succeed(Utils.Tuple0)); 73 | }); 74 | } 75 | }; 76 | } 77 | 78 | return localRuntime.Native.WebAPI.AnimationFrame.values; 79 | }; 80 | -------------------------------------------------------------------------------- /src/Native/WebAPI/Date.js: -------------------------------------------------------------------------------- 1 | Elm.Native = Elm.Native || {}; 2 | Elm.Native.WebAPI = Elm.Native.WebAPI || {}; 3 | Elm.Native.WebAPI.Date = Elm.Native.WebAPI.Date || {}; 4 | 5 | Elm.Native.WebAPI.Date.make = function (localRuntime) { 6 | localRuntime.Native = localRuntime.Native || {}; 7 | localRuntime.Native.WebAPI = localRuntime.Native.WebAPI || {}; 8 | localRuntime.Native.WebAPI.Date = localRuntime.Native.WebAPI.Date || {}; 9 | 10 | if (!localRuntime.Native.WebAPI.Date.values) { 11 | var Task = Elm.Native.Task.make(localRuntime); 12 | var Utils = Elm.Native.Utils.make(localRuntime); 13 | 14 | localRuntime.Native.WebAPI.Date.values = { 15 | decoder : function (value) { 16 | // If we are supplied an actual date, we're golden. 17 | if (value instanceof Date) { 18 | return value; 19 | } 20 | 21 | // This is what Json.Decode.decodeValue expects 22 | throw new Error( 23 | 'expecting a Date object but got ' + JSON.stringify(actual) 24 | ); 25 | }, 26 | 27 | current : Task.asyncFunction(function (callback) { 28 | callback( 29 | Task.succeed( 30 | new Date () 31 | ) 32 | ); 33 | }), 34 | 35 | now : Task.asyncFunction(function (callback) { 36 | callback( 37 | Task.succeed( 38 | Date.now() 39 | ) 40 | ); 41 | }), 42 | 43 | fromPartsLocal : function (parts) { 44 | return new Date( 45 | parts.year, 46 | parts.month, 47 | parts.day, 48 | parts.hour, 49 | parts.minute, 50 | parts.second, 51 | parts.millisecond 52 | ); 53 | }, 54 | 55 | fromPartsUtc : function (parts) { 56 | return new Date( 57 | Date.UTC( 58 | parts.year, 59 | parts.month, 60 | parts.day, 61 | parts.hour, 62 | parts.minute, 63 | parts.second, 64 | parts.millisecond 65 | ) 66 | ); 67 | }, 68 | 69 | toPartsLocal : function (date) { 70 | return { 71 | year: date.getFullYear(), 72 | month: date.getMonth(), 73 | day: date.getDate(), 74 | hour: date.getHours(), 75 | minute: date.getMinutes(), 76 | second: date.getSeconds(), 77 | millisecond: date.getMilliseconds() 78 | }; 79 | }, 80 | 81 | toPartsUtc : function (date) { 82 | return { 83 | year: date.getUTCFullYear(), 84 | month: date.getUTCMonth(), 85 | day: date.getUTCDate(), 86 | hour: date.getUTCHours(), 87 | minute: date.getUTCMinutes(), 88 | second: date.getUTCSeconds(), 89 | millisecond: date.getUTCMilliseconds() 90 | }; 91 | }, 92 | 93 | timezoneOffsetInMinutes : function (date) { 94 | return date.getTimezoneOffset(); 95 | }, 96 | 97 | dayOfWeekUTC : function (date) { 98 | return date.getUTCDay(); 99 | }, 100 | 101 | offsetYearLocal : F2(function (offset, date) { 102 | var copy = new Date(date); 103 | copy.setFullYear(date.getFullYear() + offset); 104 | 105 | // For some reason, Firefox loses the milliseconds otherwise 106 | copy.setMilliseconds(date.getMilliseconds()); 107 | 108 | return copy; 109 | }), 110 | 111 | offsetYearUTC : F2(function (offset, date) { 112 | var copy = new Date(date); 113 | copy.setUTCFullYear(date.getUTCFullYear() + offset); 114 | 115 | // For some reason, Firefox loses the milliseconds otherwise 116 | copy.setUTCMilliseconds(date.getUTCMilliseconds()); 117 | 118 | return copy; 119 | }), 120 | 121 | offsetMonthLocal : F2(function (offset, date) { 122 | var copy = new Date(date); 123 | copy.setMonth(date.getMonth() + offset); 124 | 125 | // For some reason, Firefox loses the milliseconds otherwise 126 | copy.setMilliseconds(date.getMilliseconds()); 127 | 128 | return copy; 129 | }), 130 | 131 | offsetMonthUTC : F2(function (offset, date) { 132 | var copy = new Date(date); 133 | copy.setUTCMonth(date.getUTCMonth() + offset); 134 | 135 | // For some reason, Firefox loses the milliseconds otherwise 136 | copy.setUTCMilliseconds(date.getUTCMilliseconds()); 137 | 138 | return copy; 139 | }), 140 | 141 | dateString : function (date) { 142 | return date.toDateString(); 143 | }, 144 | 145 | timeString : function (date) { 146 | return date.toTimeString(); 147 | }, 148 | 149 | isoString : function (date) { 150 | return date.toISOString(); 151 | }, 152 | 153 | utcString : function (date) { 154 | return date.toUTCString(); 155 | } 156 | }; 157 | } 158 | 159 | return localRuntime.Native.WebAPI.Date.values; 160 | }; 161 | -------------------------------------------------------------------------------- /src/Native/WebAPI/Document.js: -------------------------------------------------------------------------------- 1 | Elm.Native = Elm.Native || {}; 2 | Elm.Native.WebAPI = Elm.Native.WebAPI || {}; 3 | Elm.Native.WebAPI.Document = Elm.Native.WebAPI.Document || {}; 4 | 5 | Elm.Native.WebAPI.Document.make = function (localRuntime) { 6 | localRuntime.Native = localRuntime.Native || {}; 7 | localRuntime.Native.WebAPI = localRuntime.Native.WebAPI || {}; 8 | localRuntime.Native.WebAPI.Document = localRuntime.Native.WebAPI.Document || {}; 9 | 10 | if (!localRuntime.Native.WebAPI.Document.values) { 11 | var Task = Elm.Native.Task.make(localRuntime); 12 | var NS = Elm.Native.Signal.make(localRuntime); 13 | var Utils = Elm.Native.Utils.make(localRuntime); 14 | 15 | var Loading = {ctor: 'Loading'}; 16 | var Interactive = {ctor: 'Interactive'}; 17 | var Complete = {ctor: 'Complete'}; 18 | 19 | var getState = function () { 20 | switch (document.readyState) { 21 | case "loading": 22 | return Loading; 23 | 24 | case "interactive": 25 | return Interactive; 26 | 27 | case "complete": 28 | return Complete; 29 | 30 | default: 31 | throw new Error("Got unrecognized document.readyState: " + document.readyState); 32 | } 33 | }; 34 | 35 | var readyState = NS.input('WebAPI.Document.readyState', getState()); 36 | 37 | localRuntime.addListener([readyState.id], document, 'readystatechange', function () { 38 | localRuntime.notify(readyState.id, getState()); 39 | }); 40 | 41 | localRuntime.Native.WebAPI.Document.values = { 42 | readyState: readyState, 43 | 44 | getReadyState: Task.asyncFunction(function (callback) { 45 | callback(Task.succeed(getState())); 46 | }), 47 | 48 | getTitle : Task.asyncFunction(function (callback) { 49 | callback(Task.succeed(document.title)); 50 | }), 51 | 52 | setTitle : function (title) { 53 | return Task.asyncFunction(function (cb) { 54 | document.title = title; 55 | cb(Task.succeed(Utils.Tuple0)); 56 | }); 57 | }, 58 | 59 | events : document 60 | }; 61 | } 62 | 63 | return localRuntime.Native.WebAPI.Document.values; 64 | }; 65 | -------------------------------------------------------------------------------- /src/Native/WebAPI/Event.js: -------------------------------------------------------------------------------- 1 | Elm.Native = Elm.Native || {}; 2 | Elm.Native.WebAPI = Elm.Native.WebAPI || {}; 3 | Elm.Native.WebAPI.Event = Elm.Native.WebAPI.Event || {}; 4 | 5 | Elm.Native.WebAPI.Event.make = function (localRuntime) { 6 | localRuntime.Native = localRuntime.Native || {}; 7 | localRuntime.Native.WebAPI = localRuntime.Native.WebAPI || {}; 8 | localRuntime.Native.WebAPI.Event = localRuntime.Native.WebAPI.Event || {}; 9 | 10 | if (!localRuntime.Native.WebAPI.Event.values) { 11 | var Task = Elm.Native.Task.make(localRuntime); 12 | var Utils = Elm.Native.Utils.make(localRuntime); 13 | var JE = Elm.Native.Json.make(localRuntime); 14 | var List = Elm.List.make(localRuntime); 15 | var NL = Elm.Native.List.make(localRuntime); 16 | var Maybe = Elm.Maybe.make(localRuntime); 17 | var Basics = Elm.Basics.make(localRuntime); 18 | 19 | var toMaybe = function toMaybe (obj) { 20 | return obj === null || obj === undefined ? Maybe.Nothing : Maybe.Just(obj); 21 | }; 22 | 23 | // Copied from Native/Json.js 24 | var crash = function crash (expected, actual) { 25 | throw new Error( 26 | 'expecting ' + expected + ' but got ' + JSON.stringify(actual) 27 | ); 28 | }; 29 | 30 | localRuntime.Native.WebAPI.Event.values = { 31 | construct: F3(function (eventClass, eventName, options) { 32 | var params = JE.encodeObject(options); 33 | var args; 34 | 35 | return Task.asyncFunction(function (callback) { 36 | var event; 37 | 38 | try { 39 | event = new window[eventClass](eventName, params); 40 | } catch (ex) { 41 | event = document.createEvent(eventClass); 42 | 43 | // Don't calculate the args unless we need them, but cache 44 | // them if we do. 45 | if (!args) { 46 | var jsList = A2(List.map, Basics.snd, options); 47 | args = [eventName].concat(NL.toArray(jsList)); 48 | } 49 | 50 | event["init" + eventClass].apply(event, args); 51 | } 52 | 53 | callback(Task.succeed(event)); 54 | }); 55 | }), 56 | 57 | eventType: function (event) { 58 | return event.type; 59 | }, 60 | 61 | bubbles: function (event) { 62 | return event.bubbles; 63 | }, 64 | 65 | cancelable: function (event) { 66 | return event.cancelable; 67 | }, 68 | 69 | timestamp: function (event) { 70 | return event.timeStamp; 71 | }, 72 | 73 | eventPhase: function (event) { 74 | return event.eventPhase; 75 | }, 76 | 77 | dispatch: F2(function (target, event) { 78 | return Task.asyncFunction(function (callback) { 79 | try { 80 | var performDefaultAction = target.dispatchEvent(event); 81 | callback(Task.succeed(performDefaultAction)); 82 | } catch (ex) { 83 | callback(Task.fail(ex.toString())); 84 | } 85 | }); 86 | }), 87 | 88 | target: function (event) { 89 | return toMaybe(event.target); 90 | }, 91 | 92 | currentTarget: function (event) { 93 | return toMaybe(event.currentTarget); 94 | }, 95 | 96 | stopPropagation: function (event) { 97 | return Task.asyncFunction(function (callback) { 98 | event.stopPropagation(); 99 | callback(Utils.Tuple0); 100 | }); 101 | }, 102 | 103 | stopImmediatePropagation: function (event) { 104 | return Task.asyncFunction(function (callback) { 105 | event.stopImmediatePropagation(); 106 | callback(Utils.Tuple0); 107 | }); 108 | }, 109 | 110 | preventDefault: function (event) { 111 | return Task.asyncFunction(function (callback) { 112 | event.preventDefault(); 113 | callback(Utils.Tuple0); 114 | }); 115 | }, 116 | 117 | defaultPrevented: function (event) { 118 | return event.defaultPrevented; 119 | }, 120 | 121 | decoder: F2(function (className, value) { 122 | if (value instanceof window[className]) { 123 | return value; 124 | } 125 | 126 | crash("an " + className, value); 127 | }) 128 | }; 129 | } 130 | 131 | return localRuntime.Native.WebAPI.Event.values; 132 | }; 133 | -------------------------------------------------------------------------------- /src/Native/WebAPI/Function.js: -------------------------------------------------------------------------------- 1 | Elm.Native = Elm.Native || {}; 2 | Elm.Native.WebAPI = Elm.Native.WebAPI || {}; 3 | Elm.Native.WebAPI.Function = Elm.Native.WebAPI.Function || {}; 4 | 5 | Elm.Native.WebAPI.Function.make = function (localRuntime) { 6 | localRuntime.Native = localRuntime.Native || {}; 7 | localRuntime.Native.WebAPI = localRuntime.Native.WebAPI || {}; 8 | localRuntime.Native.WebAPI.Function = localRuntime.Native.WebAPI.Function || {}; 9 | 10 | if (!localRuntime.Native.WebAPI.Function.values) { 11 | var Task = Elm.Native.Task.make(localRuntime); 12 | var List = Elm.Native.List.make(localRuntime); 13 | var Utils = Elm.Native.Utils.make(localRuntime); 14 | var Result = Elm.Result.make(localRuntime); 15 | 16 | // Copied from Native/Json.js 17 | var crash = function crash (expected, actual) { 18 | throw new Error( 19 | 'expecting ' + expected + ' but got ' + JSON.stringify(actual) 20 | ); 21 | }; 22 | 23 | var handleResponse = function handleResponse (response) { 24 | var result; 25 | 26 | switch (response.ctor) { 27 | case 'Result': 28 | result = response._0; 29 | break; 30 | 31 | case 'Async': 32 | // Perform the task, but ignore it 33 | Task.perform(response._0); 34 | result = response._1; 35 | break; 36 | 37 | case 'Sync': 38 | // We'll use the supplied default, unless the Task is 39 | // actually synchronous, in which case we'll get that 40 | // below. 41 | result = response._1; 42 | 43 | // Construct success and failure tasks, so we can get the 44 | // success or failure values as we perform. 45 | var success = A2(Task.andThen, response._0, function (value) { 46 | result = Result.Ok(value); 47 | return Task.succeed(value); 48 | }); 49 | 50 | var failure = A2(Task.catch_, success, function (value) { 51 | result = Result.Err(value); 52 | return Task.fail(value); 53 | }); 54 | 55 | Task.perform(failure); 56 | break; 57 | 58 | default: 59 | throw new Error("Incomplete pattern match in Function.js"); 60 | } 61 | 62 | if (result.ctor === 'Ok') { 63 | return result._0; 64 | } else { 65 | throw result._0; 66 | } 67 | }; 68 | 69 | localRuntime.Native.WebAPI.Function.values = { 70 | message: function (error) { 71 | if (error.message) { 72 | return error.message; 73 | } else { 74 | return error.toString(); 75 | } 76 | }, 77 | 78 | error: function (message) { 79 | return new Error(message); 80 | }, 81 | 82 | decoder: function (value) { 83 | if (typeof value === 'function') { 84 | return value; 85 | } 86 | 87 | crash('a Javascript function', value); 88 | }, 89 | 90 | encode: function (value) { 91 | return value; 92 | }, 93 | 94 | length: function (func) { 95 | return func.length; 96 | }, 97 | 98 | apply: F3(function (self, params, func) { 99 | return Task.asyncFunction(function (callback) { 100 | try { 101 | var result = func.apply(self, List.toArray(params)); 102 | callback(Task.succeed(result)); 103 | } catch (ex) { 104 | callback(Task.fail(ex)); 105 | } 106 | }); 107 | }), 108 | 109 | pure: F3(function (self, params, func) { 110 | try { 111 | var result = func.apply(self, List.toArray(params)); 112 | return Result.Ok(result); 113 | } catch (ex) { 114 | return Result.Err(ex); 115 | } 116 | }), 117 | 118 | construct: F2(function (params, func) { 119 | return Task.asyncFunction(function (callback) { 120 | try { 121 | // We need to use `new` with an array of params. We can 122 | // do that via `bind`, since `bind` binds a function to 123 | // some params. We initially bind to a `null` this, 124 | // because `new` will supply the `this` anyway, of 125 | // course. And, we need to `apply` the bind, because we 126 | // want to supply the arguments to `bind` as an array. 127 | // 128 | // There is also probably a way to do this with 129 | // Object.create. 130 | var args = [null].concat(List.toArray(params)); 131 | var funcWithArgs = Function.prototype.bind.apply(func, args); 132 | var result = new funcWithArgs(); 133 | callback(Task.succeed(result)); 134 | } catch (ex) { 135 | callback(Task.fail(ex)); 136 | } 137 | }); 138 | }), 139 | 140 | javascript: F2(function (params, body) { 141 | try { 142 | /* jshint evil:true */ 143 | var func = new Function(List.toArray(params), body); 144 | return Result.Ok(func); 145 | } catch (ex) { 146 | return Result.Err(ex); 147 | } 148 | }), 149 | 150 | elm: function (func) { 151 | return function () { 152 | var self = this; 153 | 154 | // Json.Decode expects to see primitives, so we use valueOf. 155 | if ( 156 | self instanceof String || 157 | self instanceof Boolean || 158 | self instanceof Number 159 | ) self = self.valueOf(); 160 | 161 | // Func wants to be called with a Javascript array in which 162 | // the first element is whatever `this` is, and the rest 163 | // are the arguments. So, we construct one! 164 | var params = [self]; 165 | 166 | var length = arguments.length; 167 | for (var i = 0; i < length; i++) { 168 | params.push(arguments[i]); 169 | } 170 | 171 | var response = func(params); 172 | return handleResponse(response); 173 | }; 174 | } 175 | }; 176 | } 177 | 178 | return localRuntime.Native.WebAPI.Function.values; 179 | }; 180 | -------------------------------------------------------------------------------- /src/Native/WebAPI/Listener.js: -------------------------------------------------------------------------------- 1 | Elm.Native = Elm.Native || {}; 2 | Elm.Native.WebAPI = Elm.Native.WebAPI || {}; 3 | Elm.Native.WebAPI.Listener = Elm.Native.WebAPI.Listener || {}; 4 | 5 | Elm.Native.WebAPI.Listener.make = function (localRuntime) { 6 | localRuntime.Native = localRuntime.Native || {}; 7 | localRuntime.Native.WebAPI = localRuntime.Native.WebAPI || {}; 8 | localRuntime.Native.WebAPI.Listener = localRuntime.Native.WebAPI.Listener || {}; 9 | 10 | if (!localRuntime.Native.WebAPI.Listener.values) { 11 | var Task = Elm.Native.Task.make(localRuntime); 12 | var Utils = Elm.Native.Utils.make(localRuntime); 13 | var NS = Elm.Native.Signal.make(localRuntime); 14 | 15 | var Listener = function Listener (phase, eventName, responder, target, callback) { 16 | this.phase = phase; 17 | this.eventName = eventName; 18 | this.responder = responder; 19 | this.target = target; 20 | this.callback = callback; 21 | 22 | this.useCapture = (this.phase.ctor === 'Capture'); 23 | }; 24 | 25 | Listener.prototype.addEventListener = function () { 26 | this.target.addEventListener(this.eventName, this, this.useCapture); 27 | }; 28 | 29 | Listener.prototype.removeEventListener = function () { 30 | this.target.removeEventListener(this.eventName, this, this.useCapture); 31 | }; 32 | 33 | Listener.prototype.handleEvent = function (evt) { 34 | var responses = A2(this.responder, evt, this); 35 | 36 | this.applyResponses(evt, responses); 37 | 38 | if (this.callback) { 39 | // If we have a callback, then we're supposed to stop listening, 40 | // and use the callback 41 | this.removeEventListener(); 42 | this.callback(Task.succeed(evt)); 43 | 44 | // Just in case, get rid of the callback ... 45 | this.callback = null; 46 | } 47 | }; 48 | 49 | Listener.prototype.applyResponses = function (evt, responseList) { 50 | while (responseList.ctor !== '[]') { 51 | this.applyResponse(evt, responseList._0); 52 | responseList = responseList._1; 53 | } 54 | }; 55 | 56 | Listener.prototype.applyResponse = function (evt, response) { 57 | switch (response.ctor) { 58 | case "Set": 59 | evt[response._0] = response._1; 60 | break; 61 | 62 | case "Send": 63 | NS.sendMessage(response._0); 64 | break; 65 | 66 | case "PerformTask": 67 | Task.perform(response._0); 68 | break; 69 | 70 | case "Remove": 71 | this.removeEventListener(); 72 | break; 73 | 74 | default: 75 | throw new Error("Incomplete pattern match in Native.WebAPI.Listener"); 76 | } 77 | }; 78 | 79 | localRuntime.Native.WebAPI.Listener.values = { 80 | add: F4(function (phase, eventName, responder, target) { 81 | return Task.asyncFunction(function (callback) { 82 | var listener = new Listener(phase, eventName._0, responder, target, null); 83 | listener.addEventListener(); 84 | callback(Task.succeed(listener)); 85 | }); 86 | }), 87 | 88 | addOnce: F4(function (phase, eventName, responder, target) { 89 | return Task.asyncFunction(function (callback) { 90 | var listener = new Listener(phase, eventName._0, responder, target, callback); 91 | listener.addEventListener(); 92 | }); 93 | }), 94 | 95 | remove: function (listener) { 96 | return Task.asyncFunction(function (callback) { 97 | listener.removeEventListener(); 98 | callback(Task.succeed(Utils.Tuple0)); 99 | }); 100 | }, 101 | 102 | eventName: function (listener) { 103 | return listener.eventName; 104 | }, 105 | 106 | target: function (listener) { 107 | return listener.target; 108 | }, 109 | 110 | phase: function (listener) { 111 | return listener.phase; 112 | } 113 | }; 114 | } 115 | 116 | return localRuntime.Native.WebAPI.Listener.values; 117 | }; 118 | -------------------------------------------------------------------------------- /src/Native/WebAPI/Location.js: -------------------------------------------------------------------------------- 1 | Elm.Native = Elm.Native || {}; 2 | Elm.Native.WebAPI = Elm.Native.WebAPI || {}; 3 | Elm.Native.WebAPI.Location = Elm.Native.WebAPI.Location || {}; 4 | 5 | Elm.Native.WebAPI.Location.make = function (localRuntime) { 6 | localRuntime.Native = localRuntime.Native || {}; 7 | localRuntime.Native.WebAPI = localRuntime.Native.WebAPI || {}; 8 | localRuntime.Native.WebAPI.Location = localRuntime.Native.WebAPI.Location || {}; 9 | 10 | if (!localRuntime.Native.WebAPI.Location.values) { 11 | var Task = Elm.Native.Task.make(localRuntime); 12 | var Utils = Elm.Native.Utils.make(localRuntime); 13 | 14 | // In core before 3.0.0 15 | var copy = Utils.copy; 16 | 17 | if (!copy) { 18 | // In core from 3.0.0 19 | copy = function (value) { 20 | return Utils.update(value, {}); 21 | }; 22 | } 23 | 24 | localRuntime.Native.WebAPI.Location.values = { 25 | location: Task.asyncFunction(function (callback) { 26 | var location = copy(window.location); 27 | 28 | // Deal with Elm reserved word 29 | location.port$ = location.port; 30 | 31 | // Polyfill for IE 32 | if (!location.origin) { 33 | location.origin = 34 | location.protocol + "//" + 35 | location.hostname + 36 | (location.port ? ':' + location.port: ''); 37 | } 38 | 39 | callback(Task.succeed(location)); 40 | }), 41 | 42 | reload: function (forceServer) { 43 | return Task.asyncFunction(function (callback) { 44 | try { 45 | window.location.reload(forceServer); 46 | 47 | // Now, I suppose this won't really accomplish 48 | // anything, but let's do it anyway. 49 | callback( 50 | Task.succeed(Utils.Tuple0) 51 | ); 52 | } catch (ex) { 53 | callback( 54 | Task.fail(ex.toString()) 55 | ); 56 | } 57 | }); 58 | }, 59 | 60 | assign: function (url) { 61 | return Task.asyncFunction(function (callback) { 62 | try { 63 | window.location.assign(url); 64 | 65 | callback( 66 | Task.succeed(Utils.Tuple0) 67 | ); 68 | } catch (ex) { 69 | callback( 70 | Task.fail(ex.toString()) 71 | ); 72 | } 73 | }); 74 | }, 75 | 76 | replace: function (url) { 77 | return Task.asyncFunction(function (callback) { 78 | try { 79 | window.location.replace(url); 80 | 81 | callback( 82 | Task.succeed(Utils.Tuple0) 83 | ); 84 | } catch (ex) { 85 | callback( 86 | Task.fail(ex.toString()) 87 | ); 88 | } 89 | }); 90 | } 91 | }; 92 | } 93 | 94 | return localRuntime.Native.WebAPI.Location.values; 95 | }; 96 | -------------------------------------------------------------------------------- /src/Native/WebAPI/Math.js: -------------------------------------------------------------------------------- 1 | Elm.Native = Elm.Native || {}; 2 | Elm.Native.WebAPI = Elm.Native.WebAPI || {}; 3 | Elm.Native.WebAPI.Math = Elm.Native.WebAPI.Math || {}; 4 | 5 | Elm.Native.WebAPI.Math.make = function (localRuntime) { 6 | localRuntime.Native = localRuntime.Native || {}; 7 | localRuntime.Native.WebAPI = localRuntime.Native.WebAPI || {}; 8 | localRuntime.Native.WebAPI.Math = localRuntime.Native.WebAPI.Math || {}; 9 | 10 | if (!localRuntime.Native.WebAPI.Math.values) { 11 | var Task = Elm.Native.Task.make(localRuntime); 12 | 13 | localRuntime.Native.WebAPI.Math.values = { 14 | ln2: Math.LN2, 15 | ln10: Math.LN10, 16 | log2e: Math.LOG2E, 17 | log10e: Math.LOG10E, 18 | sqrt1_2: Math.SQRT1_2, 19 | sqrt2: Math.SQRT2, 20 | exp: Math.exp, 21 | log: Math.log, 22 | 23 | random: Task.asyncFunction(function (callback) { 24 | callback(Task.succeed(Math.random())); 25 | }) 26 | }; 27 | } 28 | 29 | return localRuntime.Native.WebAPI.Math.values; 30 | }; 31 | -------------------------------------------------------------------------------- /src/Native/WebAPI/Native.js: -------------------------------------------------------------------------------- 1 | Elm.Native = Elm.Native || {}; 2 | Elm.Native.WebAPI = Elm.Native.WebAPI || {}; 3 | Elm.Native.WebAPI.Native = Elm.Native.WebAPI.Native || {}; 4 | 5 | Elm.Native.WebAPI.Native.make = function (localRuntime) { 6 | localRuntime.Native = localRuntime.Native || {}; 7 | localRuntime.Native.WebAPI = localRuntime.Native.WebAPI || {}; 8 | localRuntime.Native.WebAPI.Native = localRuntime.Native.WebAPI.Native || {}; 9 | 10 | if (!localRuntime.Native.WebAPI.Native.values) { 11 | localRuntime.Native.WebAPI.Native.values = { 12 | unsafeCoerce: function (a) { 13 | return a; 14 | } 15 | }; 16 | } 17 | 18 | return localRuntime.Native.WebAPI.Native.values; 19 | }; 20 | -------------------------------------------------------------------------------- /src/Native/WebAPI/Number.js: -------------------------------------------------------------------------------- 1 | Elm.Native = Elm.Native || {}; 2 | Elm.Native.WebAPI = Elm.Native.WebAPI || {}; 3 | Elm.Native.WebAPI.Number = Elm.Native.WebAPI.Number || {}; 4 | 5 | Elm.Native.WebAPI.Number.make = function (localRuntime) { 6 | localRuntime.Native = localRuntime.Native || {}; 7 | localRuntime.Native.WebAPI = localRuntime.Native.WebAPI || {}; 8 | localRuntime.Native.WebAPI.Number = localRuntime.Native.WebAPI.Number || {}; 9 | 10 | if (!localRuntime.Native.WebAPI.Number.values) { 11 | var Result = Elm.Result.make(localRuntime); 12 | 13 | var toExponential = function (number) { 14 | // No try/catch needed because cannot throw exception 15 | return number.toExponential(); 16 | }; 17 | 18 | var toExponentialDigits = function (digits, number) { 19 | try { 20 | return Result.Ok(number.toExponential(digits)); 21 | } catch (ex) { 22 | return Result.Err(ex.message); 23 | } 24 | }; 25 | 26 | var toFixed = function (number) { 27 | // No try/catch needed because cannot throw exception 28 | return number.toFixed(); 29 | }; 30 | 31 | var toFixedDigits = function (digits, number) { 32 | try { 33 | return Result.Ok(number.toFixed(digits)); 34 | } catch (ex) { 35 | return Result.Err(ex.message); 36 | } 37 | }; 38 | 39 | var toPrecisionDigits = function (digits, number) { 40 | try { 41 | return Result.Ok(number.toPrecision(digits)); 42 | } catch (ex) { 43 | return Result.Err(ex.message); 44 | } 45 | }; 46 | 47 | var toStringUsingBase = function (base, number) { 48 | try { 49 | return Result.Ok(number.toString(base)); 50 | } catch (ex) { 51 | return Result.Err(ex.message); 52 | } 53 | }; 54 | 55 | localRuntime.Native.WebAPI.Number.values = { 56 | maxValue: Number.MAX_VALUE, 57 | minValue: Number.MIN_VALUE, 58 | nan: Number.NaN, 59 | negativeInfinity: Number.NEGATIVE_INFINITY, 60 | positiveInfinity: Number.POSITIVE_INFINITY, 61 | toExponential: toExponential, 62 | toExponentialDigits: F2(toExponentialDigits), 63 | toFixed: toFixed, 64 | toFixedDigits: F2(toFixedDigits), 65 | toPrecisionDigits: F2(toPrecisionDigits), 66 | toStringUsingBase: F2(toStringUsingBase) 67 | }; 68 | } 69 | 70 | return localRuntime.Native.WebAPI.Number.values; 71 | }; 72 | -------------------------------------------------------------------------------- /src/Native/WebAPI/Screen.js: -------------------------------------------------------------------------------- 1 | Elm.Native = Elm.Native || {}; 2 | Elm.Native.WebAPI = Elm.Native.WebAPI || {}; 3 | Elm.Native.WebAPI.Screen = Elm.Native.WebAPI.Screen || {}; 4 | 5 | Elm.Native.WebAPI.Screen.make = function (localRuntime) { 6 | localRuntime.Native = localRuntime.Native || {}; 7 | localRuntime.Native.WebAPI = localRuntime.Native.WebAPI || {}; 8 | localRuntime.Native.WebAPI.Screen = localRuntime.Native.WebAPI.Screen || {}; 9 | 10 | if (!localRuntime.Native.WebAPI.Screen.values) { 11 | var Task = Elm.Native.Task.make(localRuntime); 12 | var Utils = Elm.Native.Utils.make(localRuntime); 13 | 14 | var copy; 15 | if (Utils.copy) { 16 | // In core before 3.0.0 17 | copy = Utils.copy; 18 | } else { 19 | // In core from 3.0.0 20 | copy = function (value) { 21 | return Utils.update(value, {}); 22 | }; 23 | } 24 | 25 | localRuntime.Native.WebAPI.Screen.values = { 26 | // Note that this is a Task because in a multi-monitor setup, the 27 | // result depends on which monitor the browser window is being 28 | // displayed on. So, it's not a constant. 29 | // 30 | // That's also why we copy the screen object ... otherwise, it 31 | // would be *live* reference to the screen, and thus, not constant. 32 | screen: Task.asyncFunction(function (callback) { 33 | callback( 34 | Task.succeed( 35 | copy(window.screen) 36 | ) 37 | ); 38 | }), 39 | 40 | screenXY: Task.asyncFunction(function (callback) { 41 | callback( 42 | Task.succeed( 43 | Utils.Tuple2(window.screenX, window.screenY) 44 | ) 45 | ); 46 | }) 47 | }; 48 | } 49 | 50 | return localRuntime.Native.WebAPI.Screen.values; 51 | }; 52 | -------------------------------------------------------------------------------- /src/Native/WebAPI/Storage.js: -------------------------------------------------------------------------------- 1 | Elm.Native = Elm.Native || {}; 2 | Elm.Native.WebAPI = Elm.Native.WebAPI || {}; 3 | Elm.Native.WebAPI.Storage = Elm.Native.WebAPI.Storage || {}; 4 | 5 | Elm.Native.WebAPI.Storage.make = function (localRuntime) { 6 | localRuntime.Native = localRuntime.Native || {}; 7 | localRuntime.Native.WebAPI = localRuntime.Native.WebAPI || {}; 8 | localRuntime.Native.WebAPI.Storage = localRuntime.Native.WebAPI.Storage || {}; 9 | 10 | if (!localRuntime.Native.WebAPI.Storage.values) { 11 | var Task = Elm.Native.Task.make(localRuntime); 12 | var Maybe = Elm.Maybe.make(localRuntime); 13 | var NS = Elm.Native.Signal.make(localRuntime); 14 | var Utils = Elm.Native.Utils.make(localRuntime); 15 | 16 | var Local = {ctor: 'Local'}; 17 | var Session = {ctor: 'Session'}; 18 | 19 | var Disabled = {ctor: 'Disabled'}; 20 | var QuotaExceeded = {ctor: 'QuotaExceeded'}; 21 | 22 | var toMaybe = function toMaybe (obj) { 23 | return obj === null ? Maybe.Nothing : Maybe.Just(obj); 24 | }; 25 | 26 | var quotaWasExceeded = function quotaWasExceeded (e) { 27 | return e && (e.code == 22 || e.name === 'NS_ERROR_DOM_QUOTA_REACHED'); 28 | }; 29 | 30 | var hasStorage = function hasStorage () { 31 | try { 32 | // Return a boolean representing whether it's there or not. 33 | // Will throw an exception if it's disabled. 34 | return !!window.localStorage; 35 | } catch (e) { 36 | return false; 37 | } 38 | }; 39 | 40 | var toNative = function toNative (storage) { 41 | if (!hasStorage()) throw Disabled; 42 | 43 | switch (storage.ctor) { 44 | case 'Local': 45 | return window.localStorage; 46 | 47 | case 'Session': 48 | return window.sessionStorage; 49 | 50 | default: 51 | throw new Error("Incomplete pattern match in Storage.js."); 52 | } 53 | }; 54 | 55 | var fromNative = function fromNative (storage) { 56 | if (!hasStorage()) throw Disabled; 57 | 58 | if (storage == window.localStorage) { 59 | return Local; 60 | } else if (storage == window.sessionStorage) { 61 | return Session; 62 | } else { 63 | throw new Error("Got unrecognized storage type in Storage.js"); 64 | } 65 | }; 66 | 67 | var handleException = function handleException (ex, callback) { 68 | var error; 69 | 70 | if (ex == Disabled) { 71 | error = ex; 72 | } else if (quotaWasExceeded(ex)) { 73 | error = QuotaWasExceeded; 74 | } else { 75 | error = { 76 | ctor: 'Error', 77 | _0: ex.toString() 78 | }; 79 | } 80 | 81 | callback(Task.fail(error)); 82 | }; 83 | 84 | var length = function length (storage) { 85 | return Task.asyncFunction(function (callback) { 86 | try { 87 | var s = toNative(storage); 88 | callback(Task.succeed(s.length)); 89 | } catch (ex) { 90 | handleException(ex, callback); 91 | } 92 | }); 93 | }; 94 | 95 | var key = function key (storage, k) { 96 | return Task.asyncFunction(function (callback) { 97 | try { 98 | var result = null; 99 | var s = toNative(storage); 100 | 101 | // This check needed to avoid a problem in IE9 102 | if (k >= 0 && k < s.length) { 103 | result = s.key(k); 104 | } 105 | 106 | callback( 107 | Task.succeed( 108 | toMaybe(result) 109 | ) 110 | ); 111 | } catch (ex) { 112 | handleException(ex, callback); 113 | } 114 | }); 115 | }; 116 | 117 | var getItem = function getItem (storage, k) { 118 | return Task.asyncFunction(function (callback) { 119 | try { 120 | var s = toNative(storage); 121 | var result = s.getItem(k); 122 | 123 | callback( 124 | Task.succeed( 125 | toMaybe(result) 126 | ) 127 | ); 128 | } catch (ex) { 129 | handleException(ex, callback); 130 | } 131 | }); 132 | }; 133 | 134 | var setItem = function setItem (storage, k, v) { 135 | return Task.asyncFunction(function (callback) { 136 | try { 137 | var s = toNative(storage); 138 | s.setItem(k, v); 139 | callback(Task.succeed(Utils.Tuple0)); 140 | } catch (ex) { 141 | handleException(ex, callback); 142 | } 143 | }); 144 | }; 145 | 146 | var removeItem = function removeItem (storage, k) { 147 | return Task.asyncFunction(function (callback) { 148 | try { 149 | var s = toNative(storage); 150 | s.removeItem(k); 151 | callback(Task.succeed(Utils.Tuple0)); 152 | } catch (ex) { 153 | handleException(ex, callback); 154 | } 155 | }); 156 | }; 157 | 158 | var clear = function clear (storage) { 159 | return Task.asyncFunction(function (callback) { 160 | try { 161 | var s = toNative(storage); 162 | s.clear(); 163 | callback(Task.succeed(Utils.Tuple0)); 164 | } catch (ex) { 165 | handleException(ex, callback); 166 | } 167 | }); 168 | }; 169 | 170 | var enabled = Task.asyncFunction(function (callback) { 171 | callback(Task.succeed(hasStorage())); 172 | }); 173 | 174 | var events = NS.input('WebAPI.Storage.nativeEvents', Maybe.Nothing); 175 | 176 | localRuntime.addListener([events.id], window, "storage", function (event) { 177 | var e = { 178 | key: toMaybe(event.key), 179 | oldValue: toMaybe(event.oldValue), 180 | newValue: toMaybe(event.newValue), 181 | url : event.url, 182 | storageArea: fromNative(event.storageArea) 183 | }; 184 | 185 | localRuntime.notify(events.id, toMaybe(e)); 186 | }); 187 | 188 | localRuntime.Native.WebAPI.Storage.values = { 189 | length: length, 190 | key: F2(key), 191 | getItem: F2(getItem), 192 | setItem: F3(setItem), 193 | removeItem: F2(removeItem), 194 | clear: clear, 195 | enabled: enabled, 196 | 197 | nativeEvents: events 198 | }; 199 | } 200 | 201 | return localRuntime.Native.WebAPI.Storage.values; 202 | }; 203 | -------------------------------------------------------------------------------- /src/Native/WebAPI/Window.js: -------------------------------------------------------------------------------- 1 | Elm.Native = Elm.Native || {}; 2 | Elm.Native.WebAPI = Elm.Native.WebAPI || {}; 3 | Elm.Native.WebAPI.Window = Elm.Native.WebAPI.Window || {}; 4 | 5 | Elm.Native.WebAPI.Window.make = function (localRuntime) { 6 | localRuntime.Native = localRuntime.Native || {}; 7 | localRuntime.Native.WebAPI = localRuntime.Native.WebAPI || {}; 8 | localRuntime.Native.WebAPI.Window = localRuntime.Native.WebAPI.Window || {}; 9 | 10 | if (!localRuntime.Native.WebAPI.Window.values) { 11 | var Task = Elm.Native.Task.make(localRuntime); 12 | var Utils = Elm.Native.Utils.make(localRuntime); 13 | var NS = Elm.Native.Signal.make(localRuntime); 14 | 15 | var elmAlert = function (message) { 16 | return Task.asyncFunction(function (callback) { 17 | window.alert(message); 18 | callback(Task.succeed(Utils.Tuple0)); 19 | }); 20 | }; 21 | 22 | var elmConfirm = function (message) { 23 | return Task.asyncFunction(function (callback) { 24 | var result = window.confirm(message); 25 | callback( 26 | result ? Task.succeed(Utils.Tuple0) 27 | : Task.fail(Utils.Tuple0) 28 | ); 29 | }); 30 | }; 31 | 32 | var elmPrompt = function (message, defaultResponse) { 33 | return Task.asyncFunction(function (callback) { 34 | var result = window.prompt(message, defaultResponse); 35 | callback( 36 | // Safari returns "" when you press cancel, so 37 | // we need to check for that. 38 | /* jshint laxbreak: true */ 39 | result === null || result === "" 40 | ? Task.fail(Utils.Tuple0) 41 | : Task.succeed(result) 42 | ); 43 | }); 44 | }; 45 | 46 | var isOnline = Task.asyncFunction(function (callback) { 47 | if (!('onLine' in navigator)) { 48 | throw new Error("navigator.onLine was null"); 49 | } else { 50 | callback(Task.succeed(navigator.onLine)); 51 | } 52 | }); 53 | 54 | var online = NS.input('WebAPI.Window.online', navigator.onLine); 55 | 56 | localRuntime.addListener([online.id], window, 'online', function (event) { 57 | localRuntime.notify(online.id, true); 58 | }); 59 | 60 | localRuntime.addListener([online.id], window, 'offline', function (event) { 61 | localRuntime.notify(online.id, false); 62 | }); 63 | 64 | localRuntime.Native.WebAPI.Window.values = { 65 | encodeURIComponent: encodeURIComponent, 66 | decodeURIComponent: decodeURIComponent, 67 | alert: elmAlert, 68 | confirm: elmConfirm, 69 | prompt: F2(elmPrompt), 70 | events: window, 71 | isOnline: isOnline, 72 | online: online 73 | }; 74 | } 75 | 76 | return localRuntime.Native.WebAPI.Window.values; 77 | }; 78 | -------------------------------------------------------------------------------- /src/WebAPI/AnimationFrame.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.AnimationFrame 2 | ( task 3 | , Request, request, cancel 4 | ) where 5 | 6 | 7 | {-| Bindings for `window.requestAnimationFrame()` and `window.cancelAnimationFrame`. 8 | 9 | Note that 10 | [jwmerrill/elm-animation-frame](http://package.elm-lang.org/packages/jwmerrill/elm-animation-frame/latest) 11 | provides for a `Signal` of animation frames. So, this module merely provides a 12 | `Task`-oriented alternative. 13 | 14 | Other higher-level alternatives include 15 | [evancz/elm-effects](http://package.elm-lang.org/packages/evancz/elm-effects/latest) 16 | and [rgrempel/elm-ticker](https://github.com/rgrempel/elm-ticker.git). 17 | 18 | @docs task, request, Request, cancel 19 | -} 20 | 21 | 22 | import Time exposing (Time) 23 | import Task exposing (Task) 24 | import Native.WebAPI.AnimationFrame 25 | 26 | 27 | {-| A task which, when executed, will call `window.requestAnimationFrame()`. 28 | The task will complete when `requestAnimationFrame()` fires its callback, and 29 | will pass along the value provided by the callback. 30 | 31 | So, to do something when the callback fires, just add an `andThen` to the task. 32 | -} 33 | task : Task x Time 34 | task = Native.WebAPI.AnimationFrame.task 35 | 36 | 37 | {-| Opaque type which represents an animation frame request. -} 38 | type Request = Request 39 | 40 | 41 | {-| A more complex implementation of `window.requestAnimationFrame()` which 42 | allows for cancelling the request. 43 | 44 | Returns a `Task` which, when executed, will call 45 | `window.requestAnimationFrame()`, and then immediately complete with the 46 | identifier returned by `requestAnimationFrame()`. You can supply this 47 | identifier to `cancel` if you want to cancel the request. 48 | 49 | Assuming that you don't cancel the request, the following sequence of events will occur: 50 | 51 | * `window.requestAnimationFrame()` will eventually fire its callback, providing a timestamp 52 | * Your function will be called with that timestamp 53 | * The `Task` returned by your function will be immediately executed 54 | -} 55 | request : (Time -> Task x a) -> Task y Request 56 | request = Native.WebAPI.AnimationFrame.request 57 | 58 | 59 | {-| Returns a task which, when executed, will cancel the supplied request 60 | via `window.cancelAnimationFrame()`. 61 | -} 62 | cancel : Request -> Task x () 63 | cancel = Native.WebAPI.AnimationFrame.cancel 64 | -------------------------------------------------------------------------------- /src/WebAPI/Cookie.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.Cookie 2 | ( get, set 3 | , Options, setWith, defaultOptions 4 | , Error(Error, Disabled), enabled 5 | ) where 6 | 7 | {-| Wrap the browser's 8 | [`document.cookie`](https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie) 9 | object. 10 | 11 | ## Getting cookies 12 | 13 | @docs get 14 | 15 | ## Setting cookies 16 | 17 | @docs set, setWith, Options, defaultOptions 18 | 19 | ## Errors 20 | 21 | @docs Error, enabled 22 | -} 23 | 24 | import Task exposing (Task) 25 | import String exposing (split, trim, join) 26 | import Dict exposing (Dict, insert) 27 | import Time exposing (inSeconds, Time) 28 | import Date exposing (Date) 29 | import Json.Decode as JD 30 | import Json.Encode as JE 31 | import List 32 | 33 | import WebAPI.Window exposing (encodeURIComponent, decodeURIComponent) 34 | import WebAPI.Function exposing (Function) 35 | import WebAPI.Date 36 | 37 | 38 | {-| A name for a cookie. -} 39 | type alias Key = String 40 | 41 | 42 | {-| The value of a cookie. -} 43 | type alias Value = String 44 | 45 | 46 | {-| Tasks will fail with `Disabled` if the user has disabled cookies, or 47 | with `Error` for other errors. 48 | -} 49 | type Error 50 | = Disabled 51 | | Error String 52 | 53 | 54 | and = flip Task.andThen 55 | 56 | 57 | cookieEnabledDecoder : JD.Decoder Bool 58 | cookieEnabledDecoder = 59 | JD.at ["navigator", "cookieEnabled"] JD.bool 60 | 61 | 62 | {-| Whether cookies are enabled, according to the browser's 63 | `navigator.cookieEnabled`. -} 64 | enabled : Task x Bool 65 | enabled = 66 | WebAPI.Window.value 67 | |> Task.map (JD.decodeValue cookieEnabledDecoder) 68 | |> Task.map (\result -> 69 | case result of 70 | Ok bool -> 71 | bool 72 | 73 | Err error -> 74 | Debug.crash "Could not decode navigator.cookieEnabled" 75 | ) 76 | 77 | 78 | {-| A `Task` which, when executed, will succeed with the cookies, or fail with an 79 | error message if (for instance) cookies have been disabled in the browser. 80 | 81 | In the resulting `Dict`, the keys and values are the key=value pairs parsed from 82 | Javascript's `document.cookie`. The keys and values will have been uriDecoded. 83 | -} 84 | get : Task Error (Dict Key Value) 85 | get = Task.map cookieString2Dict getString 86 | 87 | 88 | cookieDecoder : JD.Decoder String 89 | cookieDecoder = 90 | JD.at ["document", "cookie"] JD.string 91 | 92 | 93 | getString : Task Error String 94 | getString = 95 | enabled `Task.andThen` \e -> 96 | if e 97 | then 98 | WebAPI.Window.value 99 | |> Task.map (JD.decodeValue cookieDecoder) 100 | |> and Task.fromResult 101 | |> Task.mapError Error 102 | 103 | else 104 | Task.fail Disabled 105 | 106 | 107 | {- We pipeline the various operations inside the foldl so that we don't 108 | iterate over the cookies more then once. Note that the uriDecode needs to 109 | happen after the split on ';' (to divide into key-value pairs) and the split on 110 | '=' (to divide the keys from the values). 111 | -} 112 | cookieString2Dict : String -> Dict Key Value 113 | cookieString2Dict = 114 | let 115 | addCookieToDict = 116 | trim >> split "=" >> List.map decodeURIComponent >> addKeyValueToDict 117 | 118 | addKeyValueToDict keyValueList = 119 | case keyValueList of 120 | key :: value :: _ -> insert key value 121 | _ -> identity 122 | 123 | in 124 | List.foldl addCookieToDict Dict.empty << split ";" 125 | 126 | 127 | {-| A task which will set a cookie using the provided key and value. The key 128 | and value will both be uriEncoded. 129 | 130 | The task will fail with an error message if cookies have been disabled in the 131 | browser. 132 | -} 133 | set : Key -> Value -> Task Error () 134 | set = setWith defaultOptions 135 | 136 | 137 | {-| Options which you can provide to setWith. -} 138 | type alias Options = 139 | { path : Maybe String 140 | , domain : Maybe String 141 | , maxAge : Maybe Time 142 | , expires : Maybe Date 143 | , secure : Maybe Bool 144 | } 145 | 146 | 147 | {-| The default options, in which all options are set to Nothing. 148 | 149 | You can use this as a starting point for `setWith`, in cases where you only 150 | want to specify some options. 151 | -} 152 | defaultOptions : Options 153 | defaultOptions = 154 | { path = Nothing 155 | , domain = Nothing 156 | , maxAge = Nothing 157 | , expires = Nothing 158 | , secure = Nothing 159 | } 160 | 161 | 162 | {-| A task which will set a cookie using the provided options, key, and value. 163 | The key and value will be uriEncoded, as well as the path and domain options 164 | (if provided). 165 | 166 | The task will fail with an error message if cookies have been disabled in 167 | the browser. 168 | -} 169 | setWith : Options -> Key -> Value -> Task Error () 170 | setWith options key value = 171 | let 172 | andThen = 173 | flip Maybe.andThen 174 | 175 | handlers = 176 | [ always <| Just <| (encodeURIComponent key) ++ "=" ++ (encodeURIComponent value) 177 | , .path >> andThen (\path -> Just <| "path=" ++ encodeURIComponent path) 178 | , .domain >> andThen (\domain -> Just <| "domain=" ++ encodeURIComponent domain) 179 | , .maxAge >> andThen (\age -> Just <| "max-age=" ++ toString (inSeconds age)) 180 | , .expires >> andThen (\expires -> Just <| "expires=" ++ WebAPI.Date.utcString expires) 181 | , .secure >> andThen (\secure -> if secure then Just "secure" else Nothing) 182 | ] 183 | 184 | cookieStrings = 185 | List.filterMap ((|>) options) handlers 186 | 187 | in 188 | setString <| join ";" cookieStrings 189 | 190 | 191 | setString : String -> Task Error () 192 | setString value = 193 | enabled 194 | |> and (\e -> 195 | if e 196 | then 197 | WebAPI.Function.apply JE.null [JE.string value] setStringFunction 198 | |> Task.mapError (WebAPI.Function.message >> Error) 199 | |> Task.map (always ()) 200 | 201 | else 202 | Task.fail Disabled 203 | ) 204 | 205 | 206 | setStringFunction : Function 207 | setStringFunction = 208 | let 209 | result = 210 | WebAPI.Function.javascript 211 | ["value"] 212 | "document.cookie = value;" 213 | 214 | in 215 | case result of 216 | Ok func -> 217 | func 218 | 219 | Err error -> 220 | Debug.crash "Error compiling perfectly good function." 221 | -------------------------------------------------------------------------------- /src/WebAPI/Document.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.Document 2 | ( ReadyState (Loading, Interactive, Complete) 3 | , readyState, getReadyState 4 | , domContentLoaded, loaded 5 | , getTitle, setTitle 6 | , target 7 | , value 8 | ) where 9 | 10 | {-| See Mozilla documentation for the 11 | [`Document` object](https://developer.mozilla.org/en-US/docs/Web/API/Document). 12 | 13 | ## Loading 14 | 15 | @docs ReadyState, readyState, getReadyState 16 | @docs domContentLoaded, loaded 17 | 18 | ## Title 19 | 20 | @docs getTitle, setTitle 21 | 22 | ## Events 23 | 24 | @docs target 25 | 26 | ## JSON 27 | 28 | @docs value 29 | -} 30 | 31 | 32 | import Signal exposing (Signal) 33 | import Task exposing (Task, andThen) 34 | import Json.Decode 35 | import Json.Encode 36 | 37 | import WebAPI.Event exposing (Target, Listener, Responder, Event) 38 | 39 | import Native.WebAPI.Document 40 | 41 | 42 | {- ------- 43 | Loading 44 | ------- -} 45 | 46 | {-| Possible values for the browser's `document.readyState` -} 47 | type ReadyState 48 | = Loading 49 | | Interactive 50 | | Complete 51 | 52 | 53 | {-| A `Signal` of changes to the browser's `document.readyState` -} 54 | readyState : Signal ReadyState 55 | readyState = Native.WebAPI.Document.readyState 56 | 57 | 58 | {-| A task which, when executed, succeeds with the value of the browser's 59 | `document.readyState`. 60 | -} 61 | getReadyState : Task x ReadyState 62 | getReadyState = Native.WebAPI.Document.getReadyState 63 | 64 | 65 | {-| A task which succeeds when the `DOMContentLoaded` event fires. If that 66 | event has already fired, then this succeeds immediately. 67 | 68 | Note that you won't usually need this in the typical Elm application in which 69 | it is Elm itself that generates most of the DOM. In that case, you'll just 70 | want to make some `Task` run when the app starts up. If you're using 71 | `StartApp`, then that would be accomplished by supplying an `Effects` as part 72 | of the `init` when you call `StartApp.start`. 73 | -} 74 | domContentLoaded : Task x () 75 | domContentLoaded = 76 | -- First we check whether it has already fired. It corresponds to the 77 | -- `Interactive` readyState, according to 78 | -- https://developer.mozilla.org/en/docs/web/api/document/readystate 79 | getReadyState `andThen` (\state -> 80 | if state == Loading 81 | then 82 | -- If it hasn't fired yet, listen for it 83 | Task.map (always ()) <| 84 | WebAPI.Event.once (WebAPI.Event.select "DOMContentLoaded") target 85 | 86 | else 87 | Task.succeed () 88 | ) 89 | 90 | 91 | {-| A task which succeeds when the `load` event fires. If that event has 92 | already fired, then this succeeds immediately. 93 | -} 94 | loaded : Task x () 95 | loaded = 96 | -- First we check whether it has already fired. It corresponds to the 97 | -- `Complete` readyState, according to 98 | -- https://developer.mozilla.org/en/docs/web/api/document/readystate 99 | getReadyState `andThen` (\state -> 100 | if state == Complete 101 | then 102 | Task.succeed () 103 | 104 | else 105 | -- If it hasn't fired yet, listen for it 106 | Task.map (always ()) <| 107 | WebAPI.Event.once (WebAPI.Event.select "load") target 108 | ) 109 | 110 | 111 | {- ------ 112 | Titles 113 | ------ -} 114 | 115 | {-| A task which, when executed, succeeds with the value of `document.title`. -} 116 | getTitle : Task x String 117 | getTitle = Native.WebAPI.Document.getTitle 118 | 119 | 120 | {-| A task which, when executed, sets the value of `document.title` to the 121 | supplied `String`. 122 | -} 123 | setTitle : String -> Task x () 124 | setTitle = Native.WebAPI.Document.setTitle 125 | 126 | 127 | {- ------ 128 | Events 129 | ------ -} 130 | 131 | {-| A target for responding to events sent to the `document` object. -} 132 | target : Target 133 | target = Native.WebAPI.Document.events 134 | 135 | 136 | {- ---- 137 | JSON 138 | ---- -} 139 | 140 | {-| Access the Javascript `document` object via `Json.Decode`. -} 141 | value : Task x Json.Decode.Value 142 | value = 143 | -- We need to put this behind a Task, because `Json.Decode` executes 144 | -- immediately, and some of the things it could access are not constants 145 | Task.succeed Native.WebAPI.Document.events 146 | -------------------------------------------------------------------------------- /src/WebAPI/Event.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.Event 2 | ( Event, eventType, bubbles, cancelable, timestamp 3 | , EventPhase(NoPhase, Capturing, AtTarget, Bubbling), eventPhase 4 | , defaultPrevented, eventTarget, listenerTarget 5 | , Options, options, defaultOptions, construct, dispatch 6 | , Target, addListener, on, addListenerOnce, once 7 | , Selector, select 8 | , Listener, listenerType, target, removeListener 9 | , ListenerPhase(Capture, Bubble), listenerPhase 10 | , Responder, noResponse 11 | , Response, preventDefault, stopPropagation, stopImmediatePropagation, set, send, performTask, remove 12 | , encode, decoder 13 | ) where 14 | 15 | {-| Support for Javascript events. 16 | 17 | There are more specific modules available for more specific types of events -- 18 | for instance, `WebAPI.Event.BeforeUnload` for the `BeforeUnloadEvent`. So, if 19 | you're interested in a specific type of event, check there first. 20 | 21 | Also, there are specific modules available for some targets. For instance, 22 | `WebAPI.Window` has some convenient event-handling methods. 23 | 24 | Furthermore, if you are using 25 | [evancz/elm-html](http://package.elm-lang.org/packages/evancz/elm-html/latest), 26 | this is not really meant for targets that are within the `Html` that your 27 | `view` function produces. For those, use `Html.Events` to deal with events. 28 | Instead, this is meant for events on target that you don't set up in your 29 | `view` function, such as the `window` and `document` etc. Though, of course, 30 | you could possibly achieve some interesting results by setting up listeners on 31 | the document or window and relying on bubbling. 32 | 33 | See Mozilla documentation for the 34 | [`EventTarget` interface](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget), 35 | and for [`Event`](https://developer.mozilla.org/en-US/docs/Web/API/Event). 36 | I also found this 37 | [list of events](http://www.w3schools.com/jsref/dom_obj_event.asp) 38 | helpful. 39 | 40 | ## Events 41 | 42 | @docs Event, eventType, bubbles, cancelable, timestamp 43 | @docs EventPhase, eventPhase 44 | @docs defaultPrevented, eventTarget, listenerTarget 45 | 46 | ## Constructing and Dispatching 47 | 48 | @docs Options, options, defaultOptions, construct, dispatch 49 | 50 | ## Listening 51 | 52 | @docs Target, addListener, on, addListenerOnce, once 53 | @docs Selector, select 54 | @docs Listener, listenerType, target, ListenerPhase, listenerPhase, removeListener 55 | 56 | ## Responding 57 | 58 | @docs Responder 59 | @docs Response, set, send, performTask, remove 60 | @docs stopPropagation, stopImmediatePropagation, preventDefault 61 | @docs noResponse 62 | 63 | ## JSON 64 | 65 | @docs encode, decoder 66 | -} 67 | 68 | 69 | import Task exposing (Task) 70 | import Time exposing (Time) 71 | import Json.Decode 72 | import Json.Encode 73 | 74 | import WebAPI.Native 75 | import WebAPI.Event.Internal 76 | import Native.WebAPI.Listener 77 | import Native.WebAPI.Event 78 | 79 | 80 | {- ----- 81 | Event 82 | ----- -} 83 | 84 | 85 | {-| Opaque type representing a Javascript event. -} 86 | type Event = Event 87 | 88 | 89 | {-| The type of the event. -} 90 | eventType : Event -> String 91 | eventType = Native.WebAPI.Event.eventType 92 | 93 | 94 | {-| Does the event bubble up through the DOM? -} 95 | bubbles : Event -> Bool 96 | bubbles = Native.WebAPI.Event.bubbles 97 | 98 | 99 | {-| Can the event be canceled? -} 100 | cancelable : Event -> Bool 101 | cancelable = Native.WebAPI.Event.cancelable 102 | 103 | 104 | {-| The time when the event was created. -} 105 | timestamp : Event -> Time 106 | timestamp = Native.WebAPI.Event.timestamp 107 | 108 | 109 | {-| The phases in which an event can be processed. -} 110 | type EventPhase 111 | = NoPhase -- 0 112 | | Capturing -- 1 113 | | AtTarget -- 2 114 | | Bubbling -- 3 115 | 116 | 117 | nativeEventPhase : Event -> Int 118 | nativeEventPhase = Native.WebAPI.Event.eventPhase 119 | 120 | 121 | toEventPhase : Int -> EventPhase 122 | toEventPhase int = 123 | case int of 124 | 0 -> NoPhase 125 | 1 -> Capturing 126 | 2 -> AtTarget 127 | 3 -> Bubbling 128 | _ -> Debug.crash ("Unexpected value for nativeEventPhase: " ++ (toString int)) 129 | 130 | 131 | {-| The phase in which the event is currently being processed. 132 | 133 | Note that typically an undispatched `Event` will return `NoPhase`, but in 134 | Opera will return `AtTarget`. 135 | -} 136 | eventPhase : Event -> EventPhase 137 | eventPhase = toEventPhase << nativeEventPhase 138 | 139 | 140 | {-| Has `preventDefault()` been called on this event? -} 141 | defaultPrevented : Event -> Bool 142 | defaultPrevented = Native.WebAPI.Event.defaultPrevented 143 | 144 | 145 | {-| The target that the event was originally dispatched to. -} 146 | eventTarget : Event -> Maybe Target 147 | eventTarget = Native.WebAPI.Event.target 148 | 149 | 150 | {-| The target that the current event listener was attached to. This may differ 151 | from the target which originally received the event, if we are in the bubbling 152 | or capturing phase. 153 | -} 154 | listenerTarget : Event -> Maybe Target 155 | listenerTarget = Native.WebAPI.Event.currentTarget 156 | 157 | 158 | {- ---------------------------- 159 | Constructing and Dispatching 160 | ---------------------------- -} 161 | 162 | 163 | {-| Create an event with the given selector and options. -} 164 | construct : Selector event -> Options event -> Task x event 165 | construct 166 | (WebAPI.Event.Internal.Selector eventName) 167 | (WebAPI.Event.Internal.Options options className) 168 | = nativeConstruct className eventName options 169 | 170 | 171 | {- The first param is the Javascript className. The second param 172 | is the event name. The third is a list of parameters, in the order 173 | in which initEvent etc. will want them. 174 | -} 175 | nativeConstruct : String -> String -> List (String, Json.Encode.Value) -> Task x event 176 | nativeConstruct = Native.WebAPI.Event.construct 177 | 178 | 179 | {-| Options for creating an event. -} 180 | type alias Options event = WebAPI.Event.Internal.Options event 181 | 182 | 183 | {-| Specify options for constructing an `Event`. -} 184 | options : {cancelable : Bool, bubbles : Bool} -> Options Event 185 | options options = 186 | -- Note that these must be in order according to what initEvent is going to want. 187 | WebAPI.Event.Internal.Options 188 | [ ("bubbles", Json.Encode.bool options.bubbles) 189 | , ("cancelable", Json.Encode.bool options.cancelable) 190 | ] 191 | "Event" 192 | 193 | 194 | {-| Default options, in which `cancelable` and `bubbles` are both false. -} 195 | defaultOptions : Options Event 196 | defaultOptions = 197 | options 198 | { cancelable = False 199 | , bubbles = False 200 | } 201 | 202 | 203 | {-| A task which dispatches an event, and completes when all the event handlers 204 | have run. The task will complete with `True` if the default action should be 205 | permitted. If any handler calls `preventDefault()`, the task will return 206 | `False`. The task will fail if certain exceptions occur. 207 | 208 | To dispatch an event from a sub-module, use the submodule's `toEvent` method. 209 | For instance, to dispatch a `CustomEvent`, do something like: 210 | 211 | dispatchCustomEvent : Target -> CustomEvent -> Task String Bool 212 | dispatchCustomEvent target customEvent = 213 | WebAPI.Event.dispatch target (WebAPI.Event.CustomEvent.toEvent customEvent) 214 | -} 215 | dispatch : Target -> Event -> Task String Bool 216 | dispatch = Native.WebAPI.Event.dispatch 217 | 218 | 219 | {- --------- 220 | Listening 221 | --------- -} 222 | 223 | {-| Opaque type which represents a Javascript object which can respond to 224 | Javascript's `addEventListener()` and `removeEventListener()`. 225 | 226 | To obtain a `Target`, see methods such as `WebAPI.Document.target` and 227 | `WebAPI.Window.target`. 228 | -} 229 | type Target = Target 230 | 231 | 232 | {-| A task which, when executed, uses Javascript's `addEventListener()` to add 233 | a `Responder` to the `Target` for the event specified by the `Selector`. 234 | 235 | Succeeds with a `Listener`, which you can supply to `removeListener` if you 236 | wish. 237 | -} 238 | addListener : ListenerPhase -> Selector event -> Responder event -> Target -> Task x Listener 239 | addListener = Native.WebAPI.Listener.add 240 | 241 | 242 | {-| Convenience method for the usual case in which you call `addListener` 243 | for the `Bubble` phase. 244 | -} 245 | on : Selector event -> Responder event -> Target -> Task x Listener 246 | on = addListener Bubble 247 | 248 | 249 | {-| Like `addListener`, but only responds to the event once, and the resulting 250 | `Task` only succeeds when the event occurs (with the value of the event object). 251 | Thus, your `Responder` method might not need to do anything. 252 | -} 253 | addListenerOnce : ListenerPhase -> Selector event -> Responder event -> Target -> Task x event 254 | addListenerOnce = Native.WebAPI.Listener.addOnce 255 | 256 | 257 | {-| Like `addListenerOnce`, but supplies the default `Phase` (`Bubble`), and a 258 | `Responder` that does nothing (so you merely chain the resulting `Task`). 259 | -} 260 | once : Selector event -> Target -> Task x event 261 | once string target = 262 | addListenerOnce Bubble string noResponse target 263 | 264 | 265 | -- This is basically here so that we can re-export it. We don't want to expose 266 | -- it from the internal module, since we don't want clients to construct it 267 | -- directly. But we can export it from here without the tag. 268 | {-| Opaque type representing an event name which uses an event type. -} 269 | type alias Selector event = WebAPI.Event.Internal.Selector event 270 | 271 | 272 | {-| Select an event name using the `Event` type. 273 | 274 | You can handle any event name with the `Event` type if you like, but often 275 | you should use a more specific event type. For instance, for 'beforeunload`, 276 | you should use `WebAPI.Event.BeforeUnload.select`, so that you can use a 277 | `BeforeUnloadEvent` in your `Responder`. 278 | -} 279 | select : String -> Selector Event 280 | select = WebAPI.Event.Internal.Selector 281 | 282 | 283 | {-| Opaque type representing an event handler. -} 284 | type Listener = Listener 285 | 286 | 287 | {-| The type of the listener's event. -} 288 | listenerType : Listener -> String 289 | listenerType = Native.WebAPI.Listener.eventName 290 | 291 | 292 | {-| The listener's target. -} 293 | target : Listener -> Target 294 | target = Native.WebAPI.Listener.target 295 | 296 | 297 | {-| The phase in which a `Responder` can be invoked. Typically, you will want `Bubble`. -} 298 | type ListenerPhase 299 | = Capture 300 | | Bubble 301 | 302 | 303 | {-| The listener's phase. -} 304 | listenerPhase : Listener -> ListenerPhase 305 | listenerPhase = Native.WebAPI.Listener.phase 306 | 307 | 308 | {-| A task which will remove the supplied `Listener`. 309 | 310 | Alternatively, you can return `remove` from your `Responder` method, and the 311 | listener will be removed. 312 | -} 313 | removeListener : Listener -> Task x () 314 | removeListener = Native.WebAPI.Listener.remove 315 | 316 | 317 | {- ---------- 318 | Responding 319 | ---------- -} 320 | 321 | 322 | {-| A function which will be called each time an event occurs, in order to 323 | determine how to respond to the event. 324 | 325 | * The `event` parameter is the Javascript event object. 326 | * The `Listener` is the listener which is responsible for this event. 327 | 328 | Your function should return a list of responses which you would like to make 329 | to the event. 330 | -} 331 | type alias Responder event = 332 | event -> Listener -> List Response 333 | 334 | 335 | {-| Opaque type which represents a response which you would like to make to an event. -} 336 | type Response 337 | = Remove 338 | | Set String Json.Encode.Value 339 | | Send Signal.Message 340 | | PerformTask (Task () ()) 341 | 342 | 343 | {-| Indicates that you would like to set a property on the event object with 344 | the specified key to the specified value. 345 | 346 | Normally, you should not need this. However, there are some events which need 347 | to be manipulated in this way -- for instance, setting the `returnValue` on the 348 | `beforeunload` event. 349 | -} 350 | set : String -> Json.Encode.Value -> Response 351 | set = Set 352 | 353 | 354 | {-| Indicates that you would like to send a message in response to the event. -} 355 | send : Signal.Message -> Response 356 | send = Send 357 | 358 | 359 | {-| Indicates that you would like to perform a `Task` in response to the event. 360 | 361 | If the task is to send a message via `Signal.send`, then you can use `send` as 362 | a convenience. 363 | -} 364 | performTask : Task () () -> Response 365 | performTask = PerformTask 366 | 367 | 368 | {-| Indicates that no longer wish to listen for this event on this target. -} 369 | remove : Response 370 | remove = Remove 371 | 372 | 373 | -- The idea here is that we don't want to expose these tasks generally, since 374 | -- they mutate the event, and we don't want to *arbitrarily* mutate the event. 375 | -- We only want to mutate the event as part of a response. So, the callback 376 | -- becomes a kind of effects interpreter, and we only expose the effects 377 | -- here, rather than the raw tasks. So, in effect, these are tasks that can 378 | -- only be performed at a certain moment. 379 | 380 | 381 | {-| Indicates that you would like to prevent further propagation of the event. -} 382 | stopPropagation : Event -> Response 383 | stopPropagation = PerformTask << stopPropagationNative 384 | 385 | 386 | stopPropagationNative : Event -> Task x () 387 | stopPropagationNative = Native.WebAPI.Event.stopPropagation 388 | 389 | 390 | {-| Like `stopPropagation`, but also prevents other listeners on the current 391 | target from being called. 392 | -} 393 | stopImmediatePropagation : Event -> Response 394 | stopImmediatePropagation = PerformTask << stopImmediatePropagationNative 395 | 396 | 397 | stopImmediatePropagationNative : Event -> Task x () 398 | stopImmediatePropagationNative = Native.WebAPI.Event.stopImmediatePropagation 399 | 400 | 401 | {-| Cancels the standard behaviour of the event. -} 402 | preventDefault : Event -> Response 403 | preventDefault = PerformTask << preventDefaultNative 404 | 405 | 406 | preventDefaultNative : Event -> Task x () 407 | preventDefaultNative = Native.WebAPI.Event.preventDefault 408 | 409 | 410 | {-| A responder that does nothing. -} 411 | noResponse : event -> Listener -> List Response 412 | noResponse event listener = [] 413 | 414 | 415 | {- ---- 416 | JSON 417 | ---- -} 418 | 419 | 420 | {-| Encode an event. -} 421 | encode : Event -> Json.Encode.Value 422 | encode = WebAPI.Native.unsafeCoerce 423 | 424 | 425 | {-| Decode an event. -} 426 | decoder : Json.Decode.Decoder Event 427 | decoder = Native.WebAPI.Event.decoder "Event" 428 | -------------------------------------------------------------------------------- /src/WebAPI/Event/BeforeUnload.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.Event.BeforeUnload 2 | ( BeforeUnloadEvent, prompt, select 3 | , toEvent, fromEvent 4 | , encode, decoder 5 | ) where 6 | 7 | 8 | {-| The browser's `BeforeUnloadEvent'. 9 | 10 | See [Mozilla documentation](https://developer.mozilla.org/en-US/docs/Web/API/BeforeUnloadEvent). 11 | 12 | See `WebAPI.Window.beforeUnload` and `WebAPI.Window.confirmUnload` for a 13 | higher-level, more convenient API. 14 | 15 | @docs BeforeUnloadEvent, prompt, select 16 | @docs toEvent, fromEvent 17 | @docs encode, decoder 18 | -} 19 | 20 | import Json.Decode 21 | import Json.Encode 22 | import Task exposing (Task) 23 | 24 | import WebAPI.Event exposing (Event, ListenerPhase(Bubble), Responder, Response, Target, Listener, noResponse) 25 | import WebAPI.Event.Internal exposing (Selector(Selector)) 26 | import WebAPI.Native 27 | import Native.WebAPI.Event 28 | 29 | 30 | {- ----------------- 31 | BeforeUnloadEvent 32 | ----------------- -} 33 | 34 | 35 | {-| Opaque type representing a BeforeUnloadEvent. -} 36 | type BeforeUnloadEvent = BeforeUnloadEvent 37 | 38 | 39 | {-| Select the 'beforeunload' event. -} 40 | select : Selector BeforeUnloadEvent 41 | select = Selector "beforeunload" 42 | 43 | 44 | {- ---------- 45 | Responding 46 | ---------- -} 47 | 48 | 49 | {-| Provide a prompt to use in the confirmation dialog box before leaving tha page. -} 50 | prompt : String -> BeforeUnloadEvent -> Response 51 | prompt string event = 52 | -- Note that event is ignored -- it's essentially for type safety 53 | WebAPI.Event.set "returnValue" <| 54 | Json.Encode.string string 55 | 56 | 57 | {- ---------- 58 | Conversion 59 | ---------- -} 60 | 61 | 62 | {-| Convert to an `Event` in order to use `Event` functions. -} 63 | toEvent : BeforeUnloadEvent -> Event 64 | toEvent = WebAPI.Native.unsafeCoerce 65 | 66 | 67 | {-| Convert from an `Event`. -} 68 | fromEvent : Event -> Maybe BeforeUnloadEvent 69 | fromEvent event = 70 | Result.toMaybe <| 71 | Json.Decode.decodeValue decoder (WebAPI.Event.encode event) 72 | 73 | 74 | {- ---- 75 | JSON 76 | ---- -} 77 | 78 | 79 | {-| Encode a BeforeUnloadEvent. -} 80 | encode : BeforeUnloadEvent -> Json.Encode.Value 81 | encode = WebAPI.Native.unsafeCoerce 82 | 83 | 84 | {-| Decode a BeforeUnloadEvent. -} 85 | decoder : Json.Decode.Decoder BeforeUnloadEvent 86 | decoder = WebAPI.Event.Internal.decoder "BeforeUnloadEvent" 87 | -------------------------------------------------------------------------------- /src/WebAPI/Event/Custom.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.Event.Custom 2 | ( CustomEvent, detail, select, options 3 | , toEvent, fromEvent 4 | , encode, decoder 5 | ) where 6 | 7 | 8 | {-| The browser's `CustomEvent'. 9 | 10 | See [Mozilla documentation](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent). 11 | 12 | @docs CustomEvent, detail 13 | @docs options, select 14 | @docs toEvent, fromEvent 15 | @docs encode, decoder 16 | -} 17 | 18 | import Json.Decode exposing ((:=)) 19 | import Json.Encode 20 | import Task exposing (Task) 21 | 22 | import WebAPI.Event exposing (Event, ListenerPhase(Bubble), Responder, Response, Target, Listener, noResponse) 23 | import WebAPI.Event.Internal exposing (Selector(Selector), Options(Options)) 24 | import WebAPI.Native 25 | import Native.WebAPI.Event 26 | 27 | 28 | {- ----------- 29 | CustomEvent 30 | ----------- -} 31 | 32 | 33 | {-| Opaque type representing a CustomEvent. -} 34 | type CustomEvent = CustomEvent 35 | 36 | 37 | {-| Data set when the `CustomEvent` was created. -} 38 | detail : CustomEvent -> Json.Decode.Value 39 | detail event = 40 | let 41 | result = 42 | Json.Decode.decodeValue 43 | ("detail" := Json.Decode.value) 44 | (encode event) 45 | 46 | in 47 | -- In principle, all CustomEvents should have a detail field. However, 48 | -- if they don't, it seems reasonable to say that it was null. 49 | Result.withDefault Json.Encode.null result 50 | 51 | 52 | {-| Specify options for creating a `CustomEvent` with the given detail. -} 53 | options : Json.Encode.Value -> Options Event -> Options CustomEvent 54 | options value (Options list _) = 55 | Options 56 | (list ++ [ ("detail", value) ]) 57 | "CustomEvent" 58 | 59 | 60 | {-| Select a `CustomEvent` with the given event type. -} 61 | select : String -> Selector CustomEvent 62 | select = Selector 63 | 64 | 65 | {- ---------- 66 | Conversion 67 | ---------- -} 68 | 69 | 70 | {-| Convert to an `Event` in order to use `Event` functions. -} 71 | toEvent : CustomEvent -> Event 72 | toEvent = WebAPI.Native.unsafeCoerce 73 | 74 | 75 | {-| Convert from an `Event`. -} 76 | fromEvent : Event -> Maybe CustomEvent 77 | fromEvent event = 78 | Result.toMaybe <| 79 | Json.Decode.decodeValue decoder (WebAPI.Event.encode event) 80 | 81 | 82 | {- ---- 83 | JSON 84 | ---- -} 85 | 86 | 87 | {-| Encode a CustomEvent. -} 88 | encode : CustomEvent -> Json.Encode.Value 89 | encode = WebAPI.Native.unsafeCoerce 90 | 91 | 92 | {-| Decode a CustomEvent. -} 93 | decoder : Json.Decode.Decoder CustomEvent 94 | decoder = WebAPI.Event.Internal.decoder "CustomEvent" 95 | -------------------------------------------------------------------------------- /src/WebAPI/Event/Internal.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.Event.Internal 2 | ( Selector (Selector) 3 | , Options (Options) 4 | , decoder 5 | ) where 6 | 7 | 8 | {-| This is a module for some things which we want to export, so they can 9 | be used in multiple modules internally, but which we don't want to make 10 | visible to clients -- so, this module is not among the 'exposed-modules' 11 | in elm-package.json. 12 | 13 | @docs selector 14 | @docs decoder 15 | -} 16 | 17 | import Json.Decode 18 | import Json.Encode 19 | 20 | import Native.WebAPI.Event 21 | 22 | 23 | {-| Binds together an event name and an event type. The idea is that we only 24 | allow the creation of selectors that bind the appropriate event names with the 25 | appropriate event types. 26 | -} 27 | type Selector event = Selector String 28 | 29 | 30 | {-| Represents an object to be used to construct an event. The first value is a 31 | list of parameters, in the order that init... expects them. The second value is 32 | the Javascript class name. 33 | -} 34 | type Options event = Options (List (String, Json.Encode.Value)) String 35 | 36 | 37 | {-| Decode an event with the given type. -} 38 | decoder : String -> Json.Decode.Decoder event 39 | decoder = Native.WebAPI.Event.decoder 40 | -------------------------------------------------------------------------------- /src/WebAPI/Function.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.Function 2 | ( Function, length, decoder, encode, apply, pure, construct 3 | , Callback, javascript, unsafeJavascript, elm 4 | , Response, return, throw, asyncAndReturn, asyncAndThrow, syncOrReturn, syncOrThrow 5 | , Error, error, message 6 | ) where 7 | 8 | {-| Support for Javascript functions. Basically, this provides two capabilities: 9 | 10 | * You can call Javascript functions from Elm. 11 | * You can provide Elm functions to Javascript to be called back from Javascript. 12 | 13 | See [Mozilla documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function) 14 | 15 | ## Types 16 | 17 | @docs Function, length, Error, error, message 18 | 19 | ## Obtaining functions from Javascript 20 | 21 | @docs javascript, unsafeJavascript, decoder 22 | 23 | ## Calling Functions 24 | 25 | @docs apply, pure, construct 26 | 27 | ## Providing functions to Javascript 28 | 29 | @docs encode, elm, Callback 30 | 31 | @docs Response, return, throw, asyncAndReturn, asyncAndThrow, syncOrReturn, syncOrThrow 32 | -} 33 | 34 | import Task exposing (Task) 35 | import Json.Encode as JE 36 | import Json.Decode as JD 37 | 38 | import Native.WebAPI.Function 39 | 40 | 41 | {- ----- 42 | Types 43 | ----- -} 44 | 45 | 46 | {-| Opaque type representing a Javascript function. -} 47 | type Function = Function 48 | 49 | 50 | {-| The number of arguments expected by a function. -} 51 | length : Function -> Int 52 | length = Native.WebAPI.Function.length 53 | 54 | 55 | {-| Opaque type representing a Javascript exception. -} 56 | type Error = Error 57 | 58 | 59 | {-| Construct an error to be thrown in a Javascript callback. Normally, one 60 | does not want errors to be thrown. However, there may be some Javascript APIs 61 | that expect callbacks to throw errors. So, this makes that possible. 62 | -} 63 | error : String -> Error 64 | error = Native.WebAPI.Function.error 65 | 66 | 67 | {-| Gets an error's message. -} 68 | message : Error -> String 69 | message = Native.WebAPI.Function.message 70 | 71 | 72 | {- ----------------------------------- 73 | Obtaining functions from Javascript 74 | ----------------------------------- -} 75 | 76 | 77 | {-| Produce a Javascript function given a list of parameter names and a 78 | Javascript function body. 79 | -} 80 | javascript : List String -> String -> Result Error Function 81 | javascript = Native.WebAPI.Function.javascript 82 | 83 | 84 | {-| Like `javascript`, but crashes if the Javascript won't compile. -} 85 | unsafeJavascript : List String -> String -> Function 86 | unsafeJavascript params body = 87 | case javascript params body of 88 | Ok func -> 89 | func 90 | 91 | Err error -> 92 | Debug.crash <| 93 | "Error while trying to compile Javascript.\n\n" ++ 94 | "Params: " ++ (toString params) ++ 95 | "Body: \n\n" ++ body ++ 96 | "\n\nMessage: " ++ (message error) 97 | 98 | 99 | {-| Extract a function. -} 100 | decoder : JD.Decoder Function 101 | decoder = Native.WebAPI.Function.decoder 102 | 103 | 104 | {- ----------------- 105 | Calling Functions 106 | ----------------- -} 107 | 108 | 109 | {-| Call a function, using the supplied value for "this", and the supplied 110 | parameters. If you don't want to supply a `this`, you could use 111 | `Json.Encode.null`. 112 | -} 113 | apply : JE.Value -> List JE.Value -> Function -> Task Error JD.Value 114 | apply = Native.WebAPI.Function.apply 115 | 116 | 117 | {-| Call a 'pure' function, using the supplied value for "this", and the 118 | supplied parameters. If you don't want to supply a `this`, you could use 119 | `Json.Encode.null`. 120 | 121 | It is your responsibility to know that the function is 'pure' -- that is: 122 | 123 | * it has no side-effects 124 | * it does not mutate its arguments (or anything else) 125 | * it returns the same value for the same arguments every time 126 | 127 | The type-checker can't verify this for you. If you have any doubt, use 128 | `apply` instead. 129 | -} 130 | pure : JE.Value -> List JE.Value -> Function -> Result Error JD.Value 131 | pure = Native.WebAPI.Function.pure 132 | 133 | 134 | {-| Use a function as a constructor, via `new`, with the supplied parameters. -} 135 | construct : List JE.Value -> Function -> Task Error JD.Value 136 | construct = Native.WebAPI.Function.construct 137 | 138 | 139 | {- --------------------------------- 140 | Providing functions to Javascript 141 | --------------------------------- -} 142 | 143 | 144 | {-| Encode a function. -} 145 | encode : Function -> JE.Value 146 | encode = Native.WebAPI.Function.encode 147 | 148 | 149 | {-| Given an Elm implementation, produce a function which can be called back 150 | from Javascript. 151 | -} 152 | elm : Callback -> Function 153 | elm = Native.WebAPI.Function.elm 154 | 155 | 156 | {-| An Elm function which can be supplied to Javascript code as a callback. 157 | 158 | When the function is invoked from Javascript, the parameter will be a 159 | Javascript array in which the first element is whatever `this` was, and the 160 | remaining elements are the parameters to the javascript function. So, you'll 161 | want to apply some `Json.Decode` decoders to get Elm types out of that. 162 | 163 | Since you're being given an array which you should independently know the 164 | length of, you'll probably want to make use of `Json.Decode.tuple1`, 165 | `Json.Decode.tuple2`, etc., depending on how many arguments you're expecting. 166 | And, remember that the first element of the Javascript array is not the first 167 | argument, but instead whatever `this` was, so you may or may not be interested 168 | in it. If you just want to ignore it, you could use `Json.Decode.succeed`. 169 | 170 | For instance, let's suppose you're expecting 2 parameters which are integers, 171 | and you don't care about `this`. In that case, you might decode with: 172 | 173 | JD.tuple3 (,,) (JD.succeed Nothing) JD.int JD.int 174 | 175 | When running `Json.Decode.decodeValue`, you'd then end up with a 176 | `(Maybe a, Int, Int)` or an error. 177 | 178 | Your Elm function should return a `Response`, which controls the return value 179 | of the Javascript function, and allows for the execution of a `Task`. 180 | -} 181 | type alias Callback = 182 | JD.Value -> Response 183 | 184 | 185 | {-| An opaque type representing your response to a function invocation from 186 | Javascript, i.e. a response to a callback. 187 | -} 188 | type Response 189 | = Result (Result Error JE.Value) 190 | | Async (Task () ()) (Result Error JE.Value) 191 | | Sync (Task Error JE.Value) (Result Error JE.Value) 192 | 193 | 194 | {-| Respond to a Javascript function call with the supplied return value. -} 195 | return : JE.Value -> Response 196 | return = Result << Result.Ok 197 | 198 | 199 | {-| Respond to a Javascript function call by throwing an error. Normally, 200 | you do not want to throw Javascript errors, but there may be some Javascript 201 | APIs that expect callbacks to do so. This makes it possible. 202 | -} 203 | throw : Error -> Response 204 | throw = Result << Result.Err 205 | 206 | 207 | {-| Respond to a Javascript function call using the supplied return value, 208 | and also perform a `Task`. 209 | 210 | This is like `return`, except that the supplied `Task` is also immediately 211 | performed when the Javascript function is called. The `Task` is presumed to be 212 | asynchronous, so its completion does not affect the return value used for the 213 | Javascript function. 214 | 215 | If you want to 'promote' the callback into the normal flow of your app, you 216 | might want to use `Signal.send` to send an action to an address. (Note that 217 | `Signal.send` is asynchronous). 218 | -} 219 | asyncAndReturn : Task () () -> JE.Value -> Response 220 | asyncAndReturn task value = 221 | Async task (Result.Ok value) 222 | 223 | 224 | {-| Respond to a Javascript function call by throwing an error, 225 | and also perform a `Task`. 226 | 227 | This is like `asyncReturn`, except an error will be thrown. 228 | -} 229 | asyncAndThrow : Task () () -> Error -> Response 230 | asyncAndThrow task value = 231 | Async task (Result.Err value) 232 | 233 | 234 | {-| Respond to a Javascript function call by executing a `Task`, and using 235 | the completion of the `Task` to control the function's return value. 236 | 237 | If the `Task` succeeds, the result will be used as the return value for the 238 | Javascript function. This allows you to chain other tasks in order to provide 239 | a return value -- so long as the other tasks are synchronous. 240 | 241 | If the `Task` fails, the result will be thrown as an error. Normally, one does 242 | not want to throw Javascript errors, but there may be some Javascript APIs that 243 | expect callbacks to do so. 244 | 245 | If the `Task` turns out to be asynchronous -- that is, if it fails to complete 246 | before the Javascript function returns -- then the supplied `JE.Value` will be 247 | used as the default return value. 248 | -} 249 | syncOrReturn : Task Error JE.Value -> JE.Value -> Response 250 | syncOrReturn task value = 251 | Sync task (Result.Ok value) 252 | 253 | 254 | {-| Respond to a Javascript function call by executing a `Task`, and using 255 | the completion of the `Task` to control the function's return value. 256 | 257 | If the `Task` succeeds, the result will be used as the return value for the 258 | Javascript function. This allows you to chain other tasks in order to provide 259 | a return value -- so long as the other tasks are synchronous. 260 | 261 | If the `Task` fails, the result will be thrown as an error. Normally, one does 262 | not want to throw Javascript errors, but there may be some Javascript APIs that 263 | expect callbacks to do so. 264 | 265 | If the `Task` turns out to be asynchronous -- that is, if it fails to complete 266 | before the Javascript function returns -- then the supplied `Error` will be 267 | thrown. 268 | -} 269 | syncOrThrow : Task Error JE.Value -> Error -> Response 270 | syncOrThrow task value = 271 | Sync task (Result.Err value) 272 | -------------------------------------------------------------------------------- /src/WebAPI/Location.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.Location 2 | ( Location, location 3 | , reload, Source(ForceServer, AllowCache) 4 | , assign, replace 5 | ) where 6 | 7 | 8 | {-| Facilities from the browser's `window.location` object. 9 | 10 | See the [Mozilla documentation](https://developer.mozilla.org/en-US/docs/Web/API/Location) 11 | 12 | For a `Signal`-oriented version of things you might do with `window.location`, see 13 | [TheSeamau5/elm-history](http://package.elm-lang.org/packages/TheSeamau5/elm-history/latest). 14 | 15 | @docs Location, location, reload, Source, assign, replace 16 | -} 17 | 18 | 19 | import Task exposing (Task) 20 | import Native.WebAPI.Location 21 | 22 | 23 | {-| The parts of a location object. Note `port'`, since `port` is a reserved word. -} 24 | type alias Location = 25 | { href: String 26 | , protocol: String 27 | , host: String 28 | , hostname: String 29 | , port': String 30 | , pathname: String 31 | , search: String 32 | , hash: String 33 | , origin: String 34 | } 35 | 36 | 37 | {-| The browser's `window.location` object. -} 38 | location : Task x Location 39 | location = Native.WebAPI.Location.location 40 | 41 | 42 | {-| Reloads the page from the current URL.-} 43 | reload : Source -> Task String () 44 | reload source = 45 | nativeReload <| 46 | case source of 47 | ForceServer -> True 48 | AllowCache -> False 49 | 50 | 51 | nativeReload : Bool -> Task String () 52 | nativeReload = Native.WebAPI.Location.reload 53 | 54 | 55 | {-| Whether to force `reload` to use the server, or allow the cache. -} 56 | type Source 57 | = ForceServer 58 | | AllowCache 59 | 60 | 61 | {-| A task which, when executed, loads the resource at the provided URL, 62 | or provides an error message upon failure. 63 | 64 | Note that only Firefox appears to reliably report an error -- other browsers 65 | silently fail if an invalid URL is provided. 66 | 67 | Also consider using `setPath` from 68 | [TheSeamau5/elm-history](http://package.elm-lang.org/packages/TheSeamau5/elm-history/latest). 69 | -} 70 | assign : String -> Task String () 71 | assign = Native.WebAPI.Location.assign 72 | 73 | 74 | {-| Like `assign`, loads the resource at the provided URL, but replaces the 75 | current page in the browser's history. 76 | 77 | Note that only Firefox appears to reliably report an error -- other browsers 78 | silently fail if an invalid URL is provided. 79 | 80 | Also consider using `replacePath` from 81 | [TheSeamau5/elm-history](http://package.elm-lang.org/packages/TheSeamau5/elm-history/latest). 82 | -} 83 | replace : String -> Task String () 84 | replace = Native.WebAPI.Location.replace 85 | -------------------------------------------------------------------------------- /src/WebAPI/Math.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.Math 2 | ( ln2, ln10, log2e, log10e, sqrt1_2, sqrt2 3 | , exp, log 4 | , random 5 | ) where 6 | 7 | 8 | {-| Various facilities from the browser's `Math` object that are not 9 | otherwise available in Elm. 10 | 11 | See the [Mozilla documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math). 12 | 13 | ## Constants 14 | 15 | @docs ln2, ln10, log2e, log10e, sqrt1_2, sqrt2 16 | 17 | ## Functions 18 | 19 | @docs exp, log 20 | 21 | ## Task 22 | 23 | @docs random 24 | -} 25 | 26 | 27 | import Task exposing (Task) 28 | import Native.WebAPI.Math 29 | 30 | 31 | {-| Natural logarithm of 2, approximately 0.693. -} 32 | ln2 : Float 33 | ln2 = Native.WebAPI.Math.ln2 34 | 35 | 36 | {-| Natural logarithm of 10, approximately 2.303. -} 37 | ln10 : Float 38 | ln10 = Native.WebAPI.Math.ln10 39 | 40 | 41 | {-| Base 2 logarithm of E, approximately 1.443. -} 42 | log2e : Float 43 | log2e = Native.WebAPI.Math.log2e 44 | 45 | 46 | {-| Base 10 logarithm of E, approximately 0.434 -} 47 | log10e : Float 48 | log10e = Native.WebAPI.Math.log10e 49 | 50 | 51 | {-| Square root of 1/2; equivalently, 1 over the square root of 2, 52 | approximately 0.707. 53 | -} 54 | sqrt1_2 : Float 55 | sqrt1_2 = Native.WebAPI.Math.sqrt1_2 56 | 57 | 58 | {-| Square root of 2, approximately 1.414. -} 59 | sqrt2 : Float 60 | sqrt2 = Native.WebAPI.Math.sqrt2 61 | 62 | 63 | {-| Returns E to the power of x, where x is the argument, and E is Euler's 64 | constant (2.718…), the base of the natural logarithm. 65 | -} 66 | exp : number -> Float 67 | exp = Native.WebAPI.Math.exp 68 | 69 | 70 | {-| Returns the natural logarithm (loge, also ln) of a number. -} 71 | log : number -> Float 72 | log = Native.WebAPI.Math.log 73 | 74 | 75 | {-| Returns a pseudo-random number between 0 and 1. 76 | 77 | Note that there is a more sophisticated implementation of `Random` in 78 | elm-lang/core. However, this may sometimes be useful if you're in a `Task` 79 | context anyway. 80 | -} 81 | random : Task x Float 82 | random = Native.WebAPI.Math.random 83 | 84 | -------------------------------------------------------------------------------- /src/WebAPI/Native.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.Native 2 | ( unsafeCoerce 3 | ) where 4 | 5 | {-| These represent functions which should only be used as a substitute for 6 | otherwise needing to write 'native' code. That is, these functions are just as 7 | dangerous as native code is -- they are not subject to the usual Elm 8 | guarantees. However, they are no worse than *actually* writing native code. 9 | That is, the ways in which you have to be careful with these functions are 10 | exactly the same ways you have to be careful with native code. So, these 11 | are meant for use only when you would otherwise be writing 'native' code. 12 | 13 | @docs unsafeCoerce 14 | -} 15 | 16 | 17 | import Native.WebAPI.Native 18 | 19 | 20 | {-| Like `Basics.identity`, but will accept any type and return any type. So, 21 | this is for cases where you know that you have something of a particular type, 22 | but (for some reason) the type checker does not know that. As the name implies, 23 | this is "unsafe" in the sense that the type checker does not verify that the 24 | types line up. So, it's sort of like `Debug.crash`, in the sense that it permits 25 | you to express some things which you know that the type checker can't understand. 26 | 27 | This is equivalent to any use of native code, since the type checker also 28 | cannot check that the values returned from native code are of the correct type. 29 | -} 30 | unsafeCoerce : a -> b 31 | unsafeCoerce = Native.WebAPI.Native.unsafeCoerce 32 | -------------------------------------------------------------------------------- /src/WebAPI/Number.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.Number 2 | ( maxValue, minValue, nan, negativeInfinity, positiveInfinity 3 | , toExponential, toExponentialDigits, safeExponentialDigits 4 | , toFixed, toFixedDigits, safeFixedDigits 5 | , toPrecisionDigits, safePrecisionDigits 6 | , toStringUsingBase, safeStringUsingBase 7 | ) where 8 | 9 | 10 | {-| Various facilities from the browser's `Number` object that are not 11 | otherwise available in Elm. 12 | 13 | See the [Mozilla documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number). 14 | 15 | ## Constants 16 | 17 | @docs maxValue, minValue, nan, negativeInfinity, positiveInfinity 18 | 19 | ## Functions 20 | 21 | @docs toExponential, toExponentialDigits, safeExponentialDigits 22 | @docs toFixed, toFixedDigits, safeFixedDigits 23 | @docs toPrecisionDigits, safePrecisionDigits 24 | @docs toStringUsingBase, safeStringUsingBase 25 | -} 26 | 27 | 28 | import Result exposing (Result) 29 | import Debug 30 | import Native.WebAPI.Number 31 | 32 | 33 | {-| The largest positive representable number. -} 34 | maxValue : Float 35 | maxValue = Native.WebAPI.Number.maxValue 36 | 37 | 38 | {-| The smallest positive representable number - that is, the positive number 39 | closest to zero (without actually being zero). 40 | -} 41 | minValue : Float 42 | minValue = Native.WebAPI.Number.minValue 43 | 44 | 45 | {-| Special "not a number" value. -} 46 | nan : Float 47 | nan = Native.WebAPI.Number.nan 48 | 49 | 50 | {-| Special value representing negative infinity; returned on overflow. -} 51 | negativeInfinity : Float 52 | negativeInfinity = Native.WebAPI.Number.negativeInfinity 53 | 54 | 55 | {-| Special value representing infinity; returned on overflow. -} 56 | positiveInfinity : Float 57 | positiveInfinity = Native.WebAPI.Number.positiveInfinity 58 | 59 | 60 | {-| A string representing the provided number in exponential notation. -} 61 | toExponential : number -> String 62 | toExponential = Native.WebAPI.Number.toExponential 63 | 64 | 65 | {-| Either a string representing the second parameter in exponential notation, 66 | with the requested number of digits after the decimal point (first parameter), 67 | or an error. An error should not occur if the requested number of digits is 68 | between 0 and 20. 69 | -} 70 | toExponentialDigits : Int -> number -> Result String String 71 | toExponentialDigits = Native.WebAPI.Number.toExponentialDigits 72 | 73 | 74 | {-| A string representing the second parameter in exponential notation, 75 | with the requested number of digits after the decimal point (first parameter). 76 | The number of digits will be limited to between 0 and 20. 77 | -} 78 | safeExponentialDigits : Int -> number -> String 79 | safeExponentialDigits digits num = 80 | case toExponentialDigits (clamp 0 20 digits) num of 81 | Ok value -> 82 | value 83 | 84 | Err err -> 85 | Debug.crash "Unexpected error: " ++ err 86 | 87 | 88 | {-| A string representing the provided number in fixed-point notation. -} 89 | toFixed : number -> String 90 | toFixed = Native.WebAPI.Number.toFixed 91 | 92 | 93 | {-| Either a string representing the second parameter in fixed-point notation, 94 | with the requested number of digits after the decimal point (first parameter), 95 | or an error. An error should not occur if the requested number of digits is 96 | between 0 and 20. 97 | 98 | Note that Firefox returns `Ok 0` rather than an `Error` for a negative number 99 | of requested digits. 100 | -} 101 | toFixedDigits : Int -> number -> Result String String 102 | toFixedDigits = Native.WebAPI.Number.toFixedDigits 103 | 104 | 105 | {-| A string representing the second parameter in fixed-point notation, 106 | with the requested number of digits after the decimal point (first parameter). 107 | The number of digits will be limited to between 0 and 20. 108 | -} 109 | safeFixedDigits : Int -> number -> String 110 | safeFixedDigits digits num = 111 | case toFixedDigits (clamp 0 20 digits) num of 112 | Ok value -> 113 | value 114 | 115 | Err err -> 116 | Debug.crash "Unexpected error: " ++ err 117 | 118 | 119 | {-| Either a string representing the second parameter in fixed-point or 120 | exponential notation, with the requested number of significant digits (first 121 | parameter), or an error. An error should not occur if the requested number of 122 | digits is between 1 and 20. 123 | -} 124 | toPrecisionDigits : Int -> number -> Result String String 125 | toPrecisionDigits = Native.WebAPI.Number.toPrecisionDigits 126 | 127 | 128 | {-| A string representing the second parameter in fixed-point or exponential 129 | notation, with the requested number of significant digits (first parameter). 130 | The number of digits will be limited to between 1 and 20. 131 | -} 132 | safePrecisionDigits : Int -> number -> String 133 | safePrecisionDigits digits num = 134 | case toPrecisionDigits (clamp 1 20 digits) num of 135 | Ok value -> 136 | value 137 | 138 | Err err -> 139 | Debug.crash "Unexpected error: " ++ err 140 | 141 | 142 | {-| Either a string representing the second parameter, using the requested base 143 | (first parameter), or an error. An error should not occur if the requested base 144 | is between 2 and 36. 145 | -} 146 | toStringUsingBase : Int -> number -> Result String String 147 | toStringUsingBase = Native.WebAPI.Number.toStringUsingBase 148 | 149 | 150 | {-| A string representing the second parameter, using the requested base 151 | (first parameter). The requested base will be limited to between 2 and 36. 152 | -} 153 | safeStringUsingBase : Int -> number -> String 154 | safeStringUsingBase base num = 155 | case toStringUsingBase (clamp 2 36 base) num of 156 | Ok value -> 157 | value 158 | 159 | Err err -> 160 | Debug.crash "Unexpected error: " ++ err 161 | -------------------------------------------------------------------------------- /src/WebAPI/Screen.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.Screen 2 | ( Screen, screen 3 | , screenXY 4 | ) where 5 | 6 | 7 | {-| The browser's `Screen` type from `window.screen`. 8 | 9 | See the [Mozilla documentation](https://developer.mozilla.org/en-US/docs/Web/API/Screen). 10 | 11 | @docs Screen, screen 12 | 13 | @docs screenXY 14 | -} 15 | 16 | import Task exposing (Task) 17 | 18 | import Native.WebAPI.Screen 19 | 20 | 21 | {-| The browser's `Screen` type. -} 22 | type alias Screen = 23 | { availTop: Int 24 | , availLeft: Int 25 | , availHeight: Int 26 | , availWidth: Int 27 | , colorDepth: Int 28 | , pixelDepth: Int 29 | , height: Int 30 | , width: Int 31 | } 32 | 33 | 34 | {-| The browser's `window.screen` object. 35 | 36 | This is a `Task` because in multi-monitor setups, the result depends on which screen 37 | the browser window is in. So, it is not necessarily a constant. 38 | -} 39 | screen : Task x Screen 40 | screen = Native.WebAPI.Screen.screen 41 | 42 | 43 | {-| A tuple of the browser's `(window.screenX, window.screenY)`. 44 | 45 | The first value is the horizontal distance, in CSS pixels, of the left 46 | border of the user's browser from the left side of the screen. 47 | 48 | The second value is the vertical distance, in CSS pixels, of the top border 49 | of the user's browser from the top edge of the screen. 50 | -} 51 | screenXY : Task x (Int, Int) 52 | screenXY = Native.WebAPI.Screen.screenXY 53 | -------------------------------------------------------------------------------- /src/WebAPI/Storage.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.Storage 2 | ( Storage(Local, Session), local, session 3 | , length, key, get, set, remove, clear 4 | , events, Event, Change(Add, Remove, Modify, Clear) 5 | , Key, OldValue, NewValue, Value 6 | , Error(Disabled, QuotaExceeded, Error), enabled 7 | ) where 8 | 9 | 10 | {-| Facilities from the browser's storage areas (`localStorage` and `sessionStorage`). 11 | 12 | See the [Mozilla documentation](https://developer.mozilla.org/en-US/docs/Web/API/Storage), 13 | and the [WhatWG documentation](https://html.spec.whatwg.org/multipage/webstorage.html). 14 | 15 | Note that there is a more sophisticated module for storage at 16 | [TheSeamau5/elm-storage](https://github.com/TheSeamau5/elm-storage) 17 | 18 | ## Storage Areas 19 | 20 | @docs Storage, local, session 21 | 22 | ## Roles for Strings 23 | 24 | @docs Key, OldValue, NewValue, Value 25 | 26 | ## Errors 27 | 28 | @docs Error, enabled 29 | 30 | ## Tasks 31 | 32 | @docs length, key, get, set, remove, clear 33 | 34 | ## Events 35 | 36 | @docs events, Event, Change 37 | -} 38 | 39 | 40 | import Task exposing (Task) 41 | import Debug 42 | import Native.WebAPI.Storage 43 | 44 | 45 | {- ----------------- 46 | Roles for Strings 47 | ----------------- -} 48 | 49 | {-| A key. -} 50 | type alias Key = String 51 | 52 | 53 | {-| An old value. -} 54 | type alias OldValue = String 55 | 56 | 57 | {-| A new value. -} 58 | type alias NewValue = String 59 | 60 | 61 | {-| A value. -} 62 | type alias Value = String 63 | 64 | 65 | {- ------------- 66 | Storage Areas 67 | ------------- -} 68 | 69 | 70 | {-| Represents the `localStorage` and `sessionStorage` areas. -} 71 | type Storage 72 | = Local 73 | | Session 74 | 75 | 76 | {-| The browser's `localStorage` area. -} 77 | local : Storage 78 | local = Local 79 | 80 | 81 | {-| The browser's `sessionStorage` area. -} 82 | session : Storage 83 | session = Session 84 | 85 | 86 | {- ------ 87 | Errors 88 | ------ -} 89 | 90 | 91 | {-| Possible error conditions. 92 | 93 | * `Disabled` indicates that the user has disabled storage. 94 | * `QuotaExceeded` indicates that the storage quota has been exceeded. 95 | * `Error` indicates that some other kind of error occurred. 96 | -} 97 | type Error 98 | = Disabled 99 | | QuotaExceeded 100 | | Error String 101 | 102 | 103 | {-| Indicates whether storage is enabled. (It can be disabled by the user). -} 104 | enabled : Task x Bool 105 | enabled = Native.WebAPI.Storage.enabled 106 | 107 | 108 | {- ----- 109 | Tasks 110 | ----- -} 111 | 112 | 113 | {-| A task which, when executed, determines the number of items stored in the 114 | storage area. 115 | -} 116 | length : Storage -> Task Error Int 117 | length = Native.WebAPI.Storage.length 118 | 119 | 120 | {-| A task which, when executed, determines the name of the key at the given 121 | index (zero-based). 122 | 123 | Succeeds with `Nothing` if the index is out of bounds. 124 | -} 125 | key : Storage -> Int -> Task Error (Maybe Key) 126 | key = Native.WebAPI.Storage.key 127 | 128 | 129 | {-| A task which, when executed, gets the value at the given key. 130 | 131 | Succeeds with `Nothing` if the key is not found. 132 | -} 133 | get : Storage -> Key -> Task Error (Maybe Value) 134 | get = Native.WebAPI.Storage.getItem 135 | 136 | 137 | {-| A task which, when executed, sets the value at the given key, or fails with 138 | an error message. 139 | -} 140 | set : Storage -> Key -> NewValue -> Task Error () 141 | set = Native.WebAPI.Storage.setItem 142 | 143 | 144 | {-| A task which, when executed, removes the item with the given key. -} 145 | remove : Storage -> Key -> Task Error () 146 | remove = Native.WebAPI.Storage.removeItem 147 | 148 | 149 | {-| A task which, when executed, removes all items. -} 150 | clear : Storage -> Task Error () 151 | clear = Native.WebAPI.Storage.clear 152 | 153 | 154 | {- ------ 155 | Events 156 | ------ -} 157 | 158 | 159 | {- This is the signal produced by the native code ... this way, we can do 160 | more of the processing in Elm, which is nicer. 161 | 162 | Note that it is a `Maybe` because Elm signals must have initial values, 163 | and there is no natural initial value for the `NativeEvent` itself unless 164 | we wrap it in a `Maybe`. 165 | -} 166 | nativeEvents : Signal (Maybe NativeEvent) 167 | nativeEvents = Native.WebAPI.Storage.nativeEvents 168 | 169 | 170 | {- An event as produced by the native code. -} 171 | type alias NativeEvent = 172 | { key : Maybe Key 173 | , oldValue : Maybe OldValue 174 | , newValue : Maybe NewValue 175 | , url : String 176 | , storageArea : Storage 177 | } 178 | 179 | 180 | {-| A storage event. -} 181 | type alias Event = 182 | { area : Storage 183 | , url : String 184 | , change : Change 185 | } 186 | 187 | 188 | {-| A change to a storage area. -} 189 | type Change 190 | = Add Key NewValue 191 | | Remove Key OldValue 192 | | Modify Key OldValue NewValue 193 | | Clear 194 | 195 | 196 | nativeEvent2Change : NativeEvent -> Change 197 | nativeEvent2Change native = 198 | case (native.key, native.oldValue, native.newValue) of 199 | (Nothing, _, _) -> 200 | Clear 201 | 202 | -- Safari does this 203 | (Just "", _, _) -> 204 | Clear 205 | 206 | (Just key, Just oldValue, Nothing) -> 207 | Remove key oldValue 208 | 209 | (Just key, Nothing, Just newValue) -> 210 | Add key newValue 211 | 212 | (Just key, Just oldValue, Just newValue) -> 213 | Modify key oldValue newValue 214 | 215 | (_, Nothing, Nothing) -> 216 | Debug.crash "The browser should never emit this." 217 | 218 | 219 | nativeEvent2Event : NativeEvent -> Event 220 | nativeEvent2Event native = 221 | { area = native.storageArea 222 | , url = native.url 223 | , change = nativeEvent2Change native 224 | } 225 | 226 | 227 | {-| A signal of storage events. 228 | 229 | Note that a storage event is not fired within the same document that made a 230 | storage change. Thus, you will only receive events for localStorage changes 231 | that occur in a **separate** tab or window. 232 | 233 | This behaviour reflects how Javascript does things ... let me know if you'd 234 | prefer to have *all* localStorage events go through this `Signal` -- it could 235 | be arranged. 236 | 237 | At least in Safari, sessionStorage is even more restrictive than localStorage 238 | -- it is isolated per-tab, so you will only get events on sessionStorage if 239 | using iframes. 240 | 241 | Note that this signal emits `Maybe Event` (rather than `Event`) because Elm 242 | signals must have an initial value -- and there is no natural initial value for 243 | an `Event` unless we wrap it in a `Maybe`. So, you'll often want to use 244 | `Signal.filterMap` when you're integrating this into your own signal of 245 | actions. 246 | 247 | If the user has disabled storage, then nothing will ever be emitted on the 248 | signal. 249 | -} 250 | events : Signal (Maybe Event) 251 | events = 252 | Signal.map (Maybe.map nativeEvent2Event) nativeEvents 253 | -------------------------------------------------------------------------------- /src/WebAPI/Window.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.Window 2 | ( alert, confirm, prompt 3 | , beforeUnload 4 | , confirmUnload, confirmUnloadOnce 5 | , onUnload, unloadOnce 6 | , target 7 | , isOnline, online 8 | , encodeURIComponent, decodeURIComponent 9 | , value 10 | ) where 11 | 12 | {-| Facilities from the browser's `window` object. 13 | 14 | See the [Mozilla documentation](https://developer.mozilla.org/en-US/docs/Web/API/Window) 15 | 16 | ## Alerts and dialogs 17 | 18 | @docs alert, confirm, prompt 19 | 20 | ## URIs 21 | 22 | @docs encodeURIComponent, decodeURIComponent 23 | 24 | ## Online status 25 | 26 | @docs isOnline, online 27 | 28 | ## Unloading 29 | 30 | @docs beforeUnload, confirmUnload, confirmUnloadOnce, onUnload, unloadOnce 31 | 32 | ## Other Events 33 | 34 | @docs target 35 | 36 | ## JSON 37 | 38 | @docs value 39 | -} 40 | 41 | 42 | import Task exposing (Task) 43 | import Json.Decode 44 | import Json.Encode 45 | 46 | import WebAPI.Event exposing (Target, Listener, Responder, Event, ListenerPhase(Bubble)) 47 | import WebAPI.Event.BeforeUnload exposing (BeforeUnloadEvent) 48 | 49 | import Native.WebAPI.Window 50 | 51 | 52 | {- ------------------ 53 | Alerts and dialogs 54 | ------------------ -} 55 | 56 | {-| The browser's `window.alert()` function. -} 57 | alert : String -> Task x () 58 | alert = Native.WebAPI.Window.alert 59 | 60 | 61 | {-| The browser's `window.confirm()` function. 62 | 63 | The task will succeed if the user confirms, and fail if the user cancels. 64 | -} 65 | confirm : String -> Task () () 66 | confirm = Native.WebAPI.Window.confirm 67 | 68 | 69 | {-| The browser's `window.prompt()` function. 70 | 71 | The first parameter is a message, and the second parameter is a default 72 | response. 73 | 74 | The task will succeed with the user's response, or fail if the user cancels 75 | or enters blank text. 76 | -} 77 | prompt : String -> String -> Task () String 78 | prompt = Native.WebAPI.Window.prompt 79 | 80 | 81 | {- ---- 82 | URIs 83 | ---- -} 84 | 85 | {-| The browser's `encodeURIComponent()`. -} 86 | encodeURIComponent : String -> String 87 | encodeURIComponent = Native.WebAPI.Window.encodeURIComponent 88 | 89 | 90 | {-| The browser's `decodeURIComponent()`. -} 91 | decodeURIComponent : String -> String 92 | decodeURIComponent = Native.WebAPI.Window.decodeURIComponent 93 | 94 | 95 | {- ------------- 96 | Online status 97 | ------------- -} 98 | 99 | {-| Whether the browser is online, according to `navigator.onLine` -} 100 | isOnline : Task x Bool 101 | isOnline = Native.WebAPI.Window.isOnline 102 | 103 | 104 | {-| A `Signal` indicating whether the browser is online, according to `navigator.onLine` -} 105 | online : Signal Bool 106 | online = Native.WebAPI.Window.online 107 | 108 | 109 | {- --------- 110 | Unloading 111 | --------- -} 112 | 113 | {-| A task which, when executed, listens for the `BeforeUnload` event. 114 | 115 | To set up a confirmation dialog, have your responder return 116 | 117 | BeforeUnload.prompt "Your message" event 118 | 119 | as one of your responses. Or, for more convenience, use `confirmUnload`. 120 | -} 121 | beforeUnload : Responder BeforeUnloadEvent -> Task x Listener 122 | beforeUnload responder = 123 | WebAPI.Event.on WebAPI.Event.BeforeUnload.select responder target 124 | 125 | 126 | {-| A task which, when executed, listens for the page to be unloaded, and 127 | requires confirmation to do so. 128 | 129 | In order to stop requiring confirmation, use `WebAPI.Event.removeListener` on 130 | the resulting listener. 131 | 132 | If you need to change the confirmation message, then you will need to use 133 | `WebAPI.Event.removeListener` to remove any existing listener, and then use 134 | this again to set up a new one. 135 | 136 | If you need to do anything more complex when `BeforeUnload` fires, then see 137 | `beforeUnload`. 138 | -} 139 | confirmUnload : String -> Task x Listener 140 | confirmUnload message = 141 | let 142 | responder event listener = 143 | [ WebAPI.Event.BeforeUnload.prompt message event 144 | ] 145 | 146 | in 147 | beforeUnload responder 148 | 149 | 150 | {-| Like `confirmUnload`, but only responds once and then removes the listener. -} 151 | confirmUnloadOnce : String -> Task x BeforeUnloadEvent 152 | confirmUnloadOnce message = 153 | let 154 | responder event listener = 155 | [ WebAPI.Event.BeforeUnload.prompt message event 156 | ] 157 | 158 | in 159 | WebAPI.Event.addListenerOnce Bubble WebAPI.Event.BeforeUnload.select responder target 160 | 161 | 162 | {-| A task which, when executed, listens for the 'unload' event. 163 | 164 | Note that it is unclear how much you can actually accomplish within 165 | the Elm architecture before the page actually unloads. Thus, you should 166 | experiment with this if you use it, and see how well it works. 167 | -} 168 | onUnload : Responder Event -> Task x Listener 169 | onUnload responder = 170 | WebAPI.Event.on (WebAPI.Event.select "unload") responder target 171 | 172 | 173 | {-| A task which, when executed, waits for the 'unload' event, and 174 | then succeeds. To do something at that time, just chain additional 175 | tasks. 176 | 177 | Note that it is unclear how much you can actually accomplish within 178 | the Elm architecture before the page actually unloads. Thus, you should 179 | experiment with this if you use it, and see how well it works. 180 | -} 181 | unloadOnce : Task x Event 182 | unloadOnce = 183 | WebAPI.Event.once (WebAPI.Event.select "unload") target 184 | 185 | 186 | {- ------------ 187 | Other Events 188 | ------------ -} 189 | 190 | {-| A target for responding to events sent to the `window` object. -} 191 | target : Target 192 | target = Native.WebAPI.Window.events 193 | 194 | 195 | {- ---- 196 | JSON 197 | ---- -} 198 | 199 | {-| Access the Javascript `window` object via `Json.Decode`. -} 200 | value : Task x Json.Decode.Value 201 | value = 202 | -- We need to put this behind a Task, because `Json.Decode` executes 203 | -- immediately, and some of the things it could access are not constants. 204 | Task.succeed Native.WebAPI.Window.events 205 | -------------------------------------------------------------------------------- /test/build/.keepme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgrempel/elm-web-api/91c0fe219325538a28d03a2b6e358c615470f6dd/test/build/.keepme -------------------------------------------------------------------------------- /test/ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Print commands as we go 4 | set -x 5 | 6 | npm run jshint || exit 1 7 | npm run instrument || exit 1 8 | 9 | elm-make src/elm/CI.elm --output build/elm.html || exit 1 10 | elm-make src/elm/DisableStorage.elm --output build/elm-disable-storage.html || exit 1 11 | elm-make src/elm/DisableRAF.elm --output build/elm-disable-raf.html || exit 1 12 | 13 | elm-make ../examples/src/WindowExample.elm --output build/window.html || exit 1 14 | elm-make ../examples/src/DocumentExample.elm --output build/document.html || exit 1 15 | elm-make ../examples/src/LocationExample.elm --output build/location.html || exit 1 16 | elm-make ../examples/src/StorageExample.elm --output build/storage.html || exit 1 17 | 18 | # Always exit 0 if we get this far ... the SauceLabs matrix takes over 19 | mocha --delay src/run.js || exit 0 20 | -------------------------------------------------------------------------------- /test/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Tests for the WebAPI module", 4 | "repository": "https://github.com/rgrempel/elm-web-api.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "build/instrumented", 8 | "src/elm", 9 | "../examples/src" 10 | ], 11 | "exposed-modules": [], 12 | "native-modules": true, 13 | "dependencies": { 14 | "Apanatshka/elm-signal-extra": "5.7.0 <= v < 6.0.0", 15 | "deadfoxygrandpa/elm-test": "2.0.0 <= v < 3.0.0", 16 | "elm-lang/core": "3.0.0 <= v < 4.0.0", 17 | "evancz/elm-html": "4.0.2 <= v < 5.0.0", 18 | "laszlopandy/elm-console": "1.0.0 <= v < 2.0.0", 19 | "evancz/elm-effects": "2.0.1 <= v < 3.0.0", 20 | "evancz/start-app": "2.0.2 <= v < 3.0.0" 21 | }, 22 | "elm-version": "0.16.0 <= v < 0.17.0" 23 | } 24 | -------------------------------------------------------------------------------- /test/jshint.json: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "window" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elm-web-api", 3 | "version": "1.0.0", 4 | "description": "Tests for elm-web-api", 5 | "main": "elm.js", 6 | "dependencies": { 7 | "chai": "^3.4.0", 8 | "colors": "^1.1.2", 9 | "count-substring": "^1.0.2", 10 | "elm": "0.16.0", 11 | "extend": "^3.0.0", 12 | "git-rev": "^0.2.1", 13 | "html-to-text": "^1.3.2", 14 | "http-server": "^0.8.5", 15 | "istanbul": "^0.4.1", 16 | "jshint": "^2.8.0", 17 | "mocha": "^2.3.3", 18 | "node-localstorage": "^0.6.0", 19 | "q": "^1.4.1", 20 | "sauce-connect-launcher": "^0.13.0", 21 | "saucelabs": "^1.0.1", 22 | "selenium-standalone": "^4.7.1", 23 | "webdriverio": "^3.2.6" 24 | }, 25 | "devDependencies": {}, 26 | "scripts": { 27 | "test": "npm run jshint && npm run instrument && elm-make src/elm/Browser.elm --output build/elm.html && open build/elm.html;", 28 | "ci": "sh ci.sh", 29 | "local": "env GEB_SAUCE_LABS_USER= sh ci.sh", 30 | "jshint": "jshint -c jshint.json ../src/Native/WebAPI/*.js src/*.js src/mocha/*.js", 31 | "instrument": "istanbul instrument --no-compact --output build/instrumented --embed-source --complete-copy ../src" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/rgrempel/elm-web-api.git" 36 | }, 37 | "author": "Ryan Rempel ", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/rgrempel/elm-web-api/issues" 41 | }, 42 | "homepage": "https://github.com/rgrempel/elm-web-api#readme" 43 | } 44 | -------------------------------------------------------------------------------- /test/src/config.js: -------------------------------------------------------------------------------- 1 | var sauceUserName = process.env.GEB_SAUCE_LABS_USER; 2 | var sauceAccessKey = process.env.GEB_SAUCE_LABS_ACCESS_PASSWORD; 3 | 4 | module.exports = { 5 | // Configuration options 6 | quiet: false, // Silences the console output 7 | 8 | webdriver: { // Options for Selenium WebDriver (WebdriverIO) 9 | user: sauceUserName, 10 | key: sauceAccessKey 11 | }, 12 | 13 | httpServer: { // Options for local http server (npmjs.org/package/http-server) 14 | disable: false, 15 | port: 8080 // Non-standard option; it is passed into the httpServer.listen() method 16 | }, 17 | 18 | sauceLabs: { // Options for SauceLabs API wrapper (npmjs.org/package/saucelabs) 19 | username: sauceUserName, 20 | password: sauceAccessKey 21 | }, 22 | 23 | sauceConnect: { // Options for SauceLabs Connect (npmjs.org/package/sauce-connect-launcher) 24 | disable: false, 25 | username: sauceUserName, 26 | accessKey: sauceAccessKey 27 | }, 28 | 29 | selenium: { // Options for Selenium Server (npmjs.org/package/selenium-standalone). Only used if you need Selenium running locally. 30 | args: [] // options to pass to `java -jar selenium-server-standalone-X.XX.X.jar` 31 | } 32 | }; 33 | 34 | -------------------------------------------------------------------------------- /test/src/coverage.js: -------------------------------------------------------------------------------- 1 | var istanbul = require('istanbul'); 2 | var collector = new istanbul.Collector(); 3 | var reporter = new istanbul.Reporter(null, "build/coverage"); 4 | 5 | module.exports = { 6 | collect : function (browser) { 7 | return browser.execute("return window.__coverage__;").then(function (obj) { 8 | collector.add(obj.value); 9 | }); 10 | }, 11 | 12 | report : function (done) { 13 | reporter.add('text'); 14 | reporter.addAll([ 'lcov', 'clover' ]); 15 | reporter.write(collector, false, function () { 16 | done(); 17 | }); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /test/src/elm/Browser.elm: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Signal exposing (Signal, Mailbox, mailbox, constant, send) 4 | import Task exposing (Task, andThen, sequence) 5 | import Graphics.Element exposing (Element, empty, flow, down) 6 | import ElmTest.Runner.Element exposing (runDisplay) 7 | import Tests 8 | 9 | 10 | import Variant exposing (Variant(..)) 11 | 12 | main : Signal Element 13 | main = 14 | let 15 | update test element = 16 | flow down 17 | [ element 18 | , runDisplay test 19 | ] 20 | 21 | in 22 | Signal.foldp update empty (.signal Tests.tests) 23 | 24 | 25 | port task : Task () () 26 | port task = Tests.task AllTests 27 | 28 | -------------------------------------------------------------------------------- /test/src/elm/CI.elm: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Signal exposing (Signal, Mailbox, mailbox, constant, send) 4 | import Task exposing (Task, andThen, sequence) 5 | import Graphics.Element exposing (Element, empty, flow, down, show) 6 | import ElmTest.Runner.String exposing (runDisplay) 7 | import Html exposing (Html, pre, text) 8 | import Html.Attributes exposing (id) 9 | import Tests 10 | 11 | import Variant exposing (Variant(..)) 12 | 13 | 14 | main : Signal Html 15 | main = 16 | let 17 | update test result = 18 | (runDisplay test) :: result 19 | 20 | models = 21 | Signal.foldp update [] (.signal Tests.tests) 22 | 23 | view model = 24 | pre [id "results"] <| 25 | List.map text model 26 | 27 | in 28 | Signal.map view models 29 | 30 | 31 | port task : Task () () 32 | port task = Tests.task AllTests 33 | 34 | -------------------------------------------------------------------------------- /test/src/elm/DisableRAF.elm: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Native.DisableRaf 4 | 5 | import Signal exposing (Signal, Mailbox, mailbox, constant, send) 6 | import Task exposing (Task, andThen, sequence) 7 | import Graphics.Element exposing (Element, empty, flow, down, show) 8 | import ElmTest.Runner.String exposing (runDisplay) 9 | import Html exposing (Html, pre, text) 10 | import Html.Attributes exposing (id) 11 | import Tests 12 | 13 | import Native.EnableRaf 14 | 15 | import Variant exposing (Variant(..)) 16 | 17 | main : Signal Html 18 | main = 19 | let 20 | update test result = 21 | (runDisplay test) :: result 22 | 23 | models = 24 | Signal.foldp update [] (.signal Tests.tests) 25 | 26 | view model = 27 | pre [id "results"] <| 28 | List.map text model 29 | 30 | in 31 | Signal.map view models 32 | 33 | 34 | port task : Task () () 35 | port task = Tests.task DisableRequestAnimationFrame 36 | 37 | -------------------------------------------------------------------------------- /test/src/elm/DisableStorage.elm: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Signal exposing (Signal, Mailbox, mailbox, constant, send) 4 | import Task exposing (Task, andThen, sequence) 5 | import Graphics.Element exposing (Element, empty, flow, down, show) 6 | import ElmTest.Runner.String exposing (runDisplay) 7 | import Html exposing (Html, pre, text) 8 | import Html.Attributes exposing (id) 9 | import Tests 10 | 11 | import Variant exposing (Variant(..)) 12 | 13 | main : Signal Html 14 | main = 15 | let 16 | update test result = 17 | (runDisplay test) :: result 18 | 19 | models = 20 | Signal.foldp update [] (.signal Tests.tests) 21 | 22 | view model = 23 | pre [id "results"] <| 24 | List.map text model 25 | 26 | in 27 | Signal.map view models 28 | 29 | 30 | port task : Task () () 31 | port task = Tests.task DisableStorage 32 | 33 | -------------------------------------------------------------------------------- /test/src/elm/Native/DisableRaf.js: -------------------------------------------------------------------------------- 1 | // This is a testing hack to artificially disable window.requestAnimationFrame 2 | 3 | window.saveAnimationFrame = window.requestAnimationFrame; 4 | window.requestAnimationFrame = null; 5 | -------------------------------------------------------------------------------- /test/src/elm/Native/EnableRaf.js: -------------------------------------------------------------------------------- 1 | // This is a testing hack to artificially re-enable window.requestAnimationFrame 2 | 3 | window.requestAnimationFrame = window.saveAnimationFrame; 4 | -------------------------------------------------------------------------------- /test/src/elm/Native/TestUtil.js: -------------------------------------------------------------------------------- 1 | Elm.Native.TestUtil = {}; 2 | Elm.Native.TestUtil.make = function (localRuntime) { 3 | localRuntime.Native = localRuntime.Native || {}; 4 | localRuntime.Native.TestUtil = localRuntime.Native.TestUtil || {}; 5 | 6 | if (!localRuntime.Native.TestUtil.values) { 7 | var Task = Elm.Native.Task.make(localRuntime); 8 | var Signal = Elm.Native.Signal.make(localRuntime); 9 | 10 | var sample = function (signal) { 11 | // Use closure to track value 12 | var val = signal.value; 13 | 14 | var handler = function (value) { 15 | val = value; 16 | }; 17 | 18 | // We construct a new "output" node, because otherwise the incoming 19 | // signal may be pruned by trimDeadNodes() in Runtime.js 20 | // (if trimDeadNodes() sees that it is not otherwise used). 21 | var output = Signal.output("sample-" + signal.name, handler, signal); 22 | 23 | return Task.asyncFunction(function (callback) { 24 | // Need to return the value inside setTimeout, because 25 | // otherwise we can be called out-of-order ... that is, a 26 | // previous `Task.andThen` which updated a Signal may not have 27 | // actually completed yet unless we do this inside a timeout. 28 | localRuntime.setTimeout(function () { 29 | callback(Task.succeed(val)); 30 | }, 0); 31 | }); 32 | }; 33 | 34 | localRuntime.Native.TestUtil.values = { 35 | sample: sample 36 | }; 37 | } 38 | 39 | return localRuntime.Native.TestUtil.values; 40 | }; 41 | -------------------------------------------------------------------------------- /test/src/elm/TestMailbox.elm: -------------------------------------------------------------------------------- 1 | module TestMailbox where 2 | 3 | import Signal exposing (Signal, Mailbox, mailbox, constant, send) 4 | import ElmTest.Test exposing (Test, suite) 5 | 6 | 7 | tests : Mailbox Test 8 | tests = 9 | mailbox (suite "Tests have not arrived yet" []) 10 | -------------------------------------------------------------------------------- /test/src/elm/TestUtil.elm: -------------------------------------------------------------------------------- 1 | module TestUtil (sample) where 2 | 3 | 4 | {-| Test utilities. 5 | 6 | @docs sample 7 | -} 8 | 9 | 10 | import Task exposing (Task) 11 | import Native.TestUtil 12 | 13 | 14 | {-| Construct a task which, when performed, will return the current value of a Signal. -} 15 | sample : Signal a -> Task x a 16 | sample = 17 | Native.TestUtil.sample 18 | -------------------------------------------------------------------------------- /test/src/elm/Tests.elm: -------------------------------------------------------------------------------- 1 | module Tests where 2 | 3 | import Signal exposing (Signal, Mailbox, mailbox, constant, send) 4 | import Task exposing (Task, andThen, sequence, onError) 5 | import ElmTest.Test exposing (Test, suite) 6 | import ElmTest.Assertion exposing (assert) 7 | 8 | import TestMailbox 9 | import Variant exposing (Variant(..)) 10 | 11 | import WebAPI.MathTest 12 | import WebAPI.NumberTest 13 | import WebAPI.StorageTest 14 | import WebAPI.ScreenTest 15 | import WebAPI.LocationTest 16 | import WebAPI.DateTest 17 | import WebAPI.AnimationFrameTest 18 | import WebAPI.CookieTest 19 | import WebAPI.DocumentTest 20 | import WebAPI.WindowTest 21 | import WebAPI.FunctionTest 22 | import WebAPI.EventTest 23 | 24 | 25 | test : Variant -> Task x Test 26 | test variant = 27 | let 28 | allTests = 29 | Task.map (suite "WebAPI tests") <| 30 | sequence tests 31 | 32 | tests = 33 | case variant of 34 | AllTests -> 35 | defaultTests 36 | 37 | DisableStorage -> 38 | testsWithStorageDisabled 39 | 40 | DisableRequestAnimationFrame -> 41 | testsWithoutRequestAnimationFrame 42 | 43 | defaultTests = 44 | [ WebAPI.DocumentTest.tests 45 | , WebAPI.MathTest.tests 46 | , WebAPI.NumberTest.tests 47 | , WebAPI.StorageTest.tests False 48 | , WebAPI.ScreenTest.tests 49 | , WebAPI.LocationTest.tests 50 | , WebAPI.DateTest.tests 51 | , WebAPI.AnimationFrameTest.tests 52 | , WebAPI.CookieTest.tests 53 | , WebAPI.WindowTest.tests 54 | , WebAPI.FunctionTest.tests 55 | , WebAPI.EventTest.tests 56 | ] 57 | 58 | testsWithStorageDisabled = 59 | [ WebAPI.StorageTest.tests True 60 | ] 61 | 62 | testsWithoutRequestAnimationFrame = 63 | [ WebAPI.AnimationFrameTest.tests 64 | ] 65 | 66 | in 67 | onError allTests <| 68 | always <| 69 | Task.succeed <| 70 | ElmTest.Test.test "Some task failed" <| 71 | assert False 72 | 73 | 74 | task : Variant -> Task x () 75 | task variant = 76 | test variant 77 | `andThen` send tests.address 78 | 79 | 80 | tests : Mailbox Test 81 | tests = TestMailbox.tests 82 | -------------------------------------------------------------------------------- /test/src/elm/Variant.elm: -------------------------------------------------------------------------------- 1 | module Variant where 2 | 3 | 4 | type Variant 5 | = AllTests 6 | | DisableStorage 7 | | DisableRequestAnimationFrame 8 | -------------------------------------------------------------------------------- /test/src/elm/WebAPI/AnimationFrameTest.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.AnimationFrameTest where 2 | 3 | import ElmTest.Test exposing (..) 4 | import ElmTest.Assertion exposing (..) 5 | import Task exposing (Task, sequence, succeed, andThen) 6 | import Time exposing (Time) 7 | import Signal exposing (Signal, Mailbox, mailbox) 8 | import String 9 | 10 | import WebAPI.AnimationFrame as AnimationFrame 11 | import WebAPI.Date 12 | import Debug 13 | import TestUtil exposing (sample) 14 | 15 | 16 | -- When testing on SauceLabs, this is really slow ... much faster locally ... 17 | -- not sure why. So, we allow an absurd amount of time for a frame here ... 18 | -- when run locally, the frame rate is about right. So, this is not really 19 | -- testing performance at the moment, just that it works at all. 20 | frame : Time 21 | frame = Time.second 22 | 23 | 24 | taskTest : Task () Test 25 | taskTest = 26 | WebAPI.Date.now `andThen` (\startTime -> 27 | AnimationFrame.task `andThen` (\timestamp -> 28 | AnimationFrame.task `andThen` (\timestamp2 -> 29 | WebAPI.Date.now `andThen` (\endTime -> 30 | let 31 | wallTime = 32 | endTime - startTime 33 | 34 | delta = 35 | timestamp2 - timestamp 36 | 37 | in 38 | Task.succeed <| 39 | suite "task" 40 | [ test 41 | ( String.join " " 42 | [ "wall time" 43 | , toString wallTime 44 | , "is less than" 45 | , toString (frame * 2) 46 | ] 47 | ) <| 48 | assert (wallTime < frame * 2) 49 | , test 50 | ( String.join " " 51 | [ "callback time" 52 | , toString delta 53 | , "is less than" 54 | , toString frame 55 | ] 56 | ) <| 57 | assert (delta < frame) 58 | ] 59 | )))) 60 | 61 | 62 | result : Mailbox Time 63 | result = mailbox 0 64 | 65 | 66 | delay : Time 67 | delay = 0.2 * Time.second 68 | 69 | 70 | requestTest : Task () Test 71 | requestTest = 72 | let 73 | task time = 74 | Signal.send result.address time 75 | 76 | in 77 | Task.map (\time -> 78 | test ("request fired at " ++ (toString time)) <| 79 | assert (time > 0) 80 | ) <| Signal.send result.address 0 81 | `andThen` always (AnimationFrame.request task) 82 | `andThen` always (Task.sleep delay) 83 | `andThen` always (sample result.signal) 84 | 85 | 86 | cancelTest : Task () Test 87 | cancelTest = 88 | let 89 | task time = 90 | Signal.send result.address time 91 | 92 | in 93 | Task.map (\time -> 94 | test "request should have been canceled" <| 95 | assertEqual 0 time 96 | ) <| Signal.send result.address 0 97 | `andThen` always (AnimationFrame.request task) 98 | `andThen` AnimationFrame.cancel 99 | `andThen` always (Task.sleep delay) 100 | `andThen` always (sample result.signal) 101 | 102 | 103 | tests : Task () Test 104 | tests = 105 | Task.map (suite "WebAPI.AnimationFrameTest") <| 106 | sequence <| 107 | [ taskTest 108 | , requestTest 109 | , cancelTest 110 | ] 111 | 112 | -------------------------------------------------------------------------------- /test/src/elm/WebAPI/CookieTest.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.CookieTest where 2 | 3 | import String 4 | import ElmTest.Assertion exposing (..) 5 | import ElmTest.Test exposing (..) 6 | import ElmTest.Runner.Element exposing (runDisplay) 7 | 8 | import Task exposing (Task, andThen, sequence, map) 9 | import Date exposing (fromTime) 10 | import Time exposing (second) 11 | import Dict 12 | 13 | import WebAPI.Cookie as Cookies 14 | 15 | 16 | (>>>) = flip Task.map 17 | (>>+) = Task.andThen 18 | (>>!) = Task.onError 19 | 20 | 21 | (>>-) task func = 22 | task `andThen` (always func) 23 | 24 | 25 | reportError : String -> Cookies.Error -> Task x Test 26 | reportError name error = 27 | Task.succeed <| 28 | test (name ++ ": " ++ (toString error)) <| 29 | assert False 30 | 31 | 32 | simpleSetGet : Task x Test 33 | simpleSetGet = 34 | Cookies.set "bog" "joe" 35 | >>- Cookies.get 36 | >>> ( 37 | Dict.get "bog" 38 | >> assertEqual (Just "joe") 39 | >> test "simple set then get" 40 | ) 41 | >>! reportError "simple set then get" 42 | 43 | 44 | -- Make sure we're actually *changing* the cookie 45 | secondSetGet : Task x Test 46 | secondSetGet = 47 | Cookies.set "bog" "frank" 48 | >>- Cookies.get 49 | >>> ( 50 | Dict.get "bog" 51 | >> assertEqual (Just "frank") 52 | >> test "repeated set/get, to make sure we can change the cookie" 53 | ) 54 | >>! reportError "repeated set/get" 55 | 56 | 57 | multipleSetGet : Task x Test 58 | multipleSetGet = 59 | Cookies.set "cookie1" "cookie 1 value" 60 | >>- Cookies.set "cookie2" "cookie 2 value" 61 | >>- Cookies.get 62 | >>> (\cookies -> 63 | [ Dict.get "cookie1" cookies 64 | , Dict.get "cookie2" cookies 65 | ] 66 | |> assertEqual 67 | [ Just "cookie 1 value" 68 | , Just "cookie 2 value" 69 | ] 70 | |> test "multiple cookies" 71 | ) 72 | >>! reportError "multiple cookies" 73 | 74 | 75 | encodingTest : Task x Test 76 | encodingTest = 77 | Cookies.set "encoded=" "value needs encoding ;" 78 | >>- Cookies.get 79 | >>> ( 80 | Dict.get "encoded=" 81 | >> assertEqual (Just "value needs encoding ;") 82 | >> test "key and value should be encoded" 83 | ) 84 | >>! reportError "key and value should be encoded" 85 | 86 | 87 | setWithWrongPath : Task x Test 88 | setWithWrongPath = 89 | let 90 | defaults = 91 | Cookies.defaultOptions 92 | 93 | options = 94 | -- { defaults | path = Just "/path" } 95 | Cookies.Options (Just "/path") Nothing Nothing Nothing Nothing 96 | 97 | in 98 | Cookies.setWith options "wrong path cookie" "path cookie value" 99 | >>- Cookies.get 100 | >>> ( 101 | Dict.get "wrong path cookie" 102 | >> assertEqual Nothing 103 | >> test "test with path set to bad value" 104 | ) 105 | >>! reportError "test with path set to bad value" 106 | 107 | 108 | setWithGoodPath : Task x Test 109 | setWithGoodPath = 110 | let 111 | defaults = 112 | Cookies.defaultOptions 113 | 114 | options = 115 | -- { defaults | path = Just "" } 116 | Cookies.Options (Just "") Nothing Nothing Nothing Nothing 117 | 118 | in 119 | Cookies.setWith options "good path cookie" "path cookie value" 120 | >>- Cookies.get 121 | >>> ( 122 | Dict.get "good path cookie" 123 | >> assertEqual (Just "path cookie value") 124 | >> test "test with path set to good value" 125 | ) 126 | >>! reportError "test with path set to good value" 127 | 128 | 129 | maxAgeFuture : Task x Test 130 | maxAgeFuture = 131 | let 132 | defaults = 133 | Cookies.defaultOptions 134 | 135 | options = 136 | -- { defaults | maxAge = Just 1000 } 137 | Cookies.Options Nothing Nothing (Just 1000) Nothing Nothing 138 | 139 | in 140 | Cookies.setWith options "max age future" "cookie value" 141 | >>- Cookies.get 142 | >>> ( 143 | Dict.get "max age future" 144 | >> assertEqual (Just "cookie value") 145 | >> test "test with maxAge in future" 146 | ) 147 | >>! reportError "test with maxAge in future" 148 | 149 | 150 | expiresInFuture : Task x Test 151 | expiresInFuture = 152 | let 153 | defaults = 154 | Cookies.defaultOptions 155 | 156 | options = 157 | -- Sets the date to September 26, 2028 conveniently 158 | -- { defaults | expires = Just (fromTime (1853609409 * second)) } 159 | Cookies.Options Nothing Nothing Nothing (Just (fromTime (1853609409 * second))) Nothing 160 | 161 | in 162 | Cookies.setWith options "expires" "cookie value" 163 | >>- Cookies.get 164 | >>> ( 165 | Dict.get "expires" 166 | >> assertEqual (Just "cookie value") 167 | >> test "test with expiry in future" 168 | ) 169 | >>! reportError "test with expiry in future" 170 | 171 | 172 | expiresInPast : Task x Test 173 | expiresInPast = 174 | let 175 | defaults = 176 | Cookies.defaultOptions 177 | 178 | options = 179 | -- { defaults | expires = Just (fromTime 0) } 180 | Cookies.Options Nothing Nothing Nothing (Just (fromTime 0)) Nothing 181 | 182 | in 183 | Cookies.setWith options "expires" "cookie value" 184 | >>- Cookies.get 185 | >>> ( 186 | Dict.get "expires" 187 | >> assertEqual Nothing 188 | >> test "test with expiry in past" 189 | ) 190 | >>! reportError "test with expiry in past" 191 | 192 | 193 | enabledTest : Task x Test 194 | enabledTest = 195 | Cookies.enabled 196 | >>> ( 197 | assert 198 | >> test "Cookies should be enabled" 199 | ) 200 | 201 | 202 | tests : Task () Test 203 | tests = 204 | Task.map (suite "WebAPI.CookieTest") <| 205 | sequence 206 | [ simpleSetGet 207 | , secondSetGet 208 | , multipleSetGet 209 | , encodingTest 210 | 211 | -- TODO: setWithWrongPath is "failing" on Chrome, but it's not really a proper 212 | -- test, since I'm just trying to retrieve it locally -- what the path really 213 | -- controls is whether it's sent to the server. So, in principle I ought to 214 | -- do a more sophisticated test of this ... 215 | -- , setWithWrongPath 216 | 217 | , setWithGoodPath 218 | , maxAgeFuture 219 | , expiresInFuture 220 | , expiresInPast 221 | , enabledTest 222 | ] 223 | -------------------------------------------------------------------------------- /test/src/elm/WebAPI/DocumentTest.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.DocumentTest where 2 | 3 | import String 4 | import ElmTest.Assertion exposing (..) 5 | import ElmTest.Test exposing (..) 6 | 7 | import Task exposing (Task, andThen, sequence, map) 8 | import TestUtil exposing (sample) 9 | import Signal.Extra exposing (foldp') 10 | import Json.Decode as JD 11 | import Time 12 | 13 | import WebAPI.Document exposing (..) 14 | 15 | 16 | (>>>) = flip Task.map 17 | (>>+) = Task.andThen 18 | 19 | 20 | (>>-) task func = 21 | task `andThen` (always func) 22 | 23 | 24 | getReadyStateTest : Task x Test 25 | getReadyStateTest = 26 | getReadyState >>> (\state -> 27 | test ("getReadyState got " ++ (toString state)) <| 28 | -- Basically, this succeeds if it doesn't throw an error 29 | assert <| 30 | state == Loading || 31 | state == Interactive || 32 | state == Complete 33 | ) 34 | 35 | 36 | readyStateTest : Task x Test 37 | readyStateTest = 38 | let 39 | accumulator = 40 | foldp' (::) (\s -> [s]) readyState 41 | 42 | in 43 | Task.sleep (0.5 * Time.second) >>- 44 | sample accumulator >>> (\list -> 45 | test ("readyState signal: " ++ (toString list)) <| 46 | assert <| 47 | List.length list >= 2 48 | ) 49 | 50 | 51 | getTitleTest : Task x Test 52 | getTitleTest = 53 | getTitle >>> (\title -> 54 | test "getTitle should be 'Main'" <| 55 | assertEqual "Main" title 56 | ) 57 | 58 | 59 | setTitleTest : Task x Test 60 | setTitleTest = 61 | setTitle "New title" >>+ (\setTitleResponse -> 62 | getTitle >>> (\newTitle -> 63 | test "setTitle should work" <| 64 | assert <| 65 | setTitleResponse == () && 66 | newTitle == "New title" 67 | ) 68 | ) 69 | 70 | 71 | testValue : Task x Test 72 | testValue = 73 | let 74 | titleDecoder = 75 | JD.at ["title"] JD.string 76 | 77 | in 78 | value 79 | |> Task.map (JD.decodeValue titleDecoder) 80 | |> Task.map (\result -> 81 | case result of 82 | Ok string -> 83 | assertEqual "Main" string 84 | 85 | Err _ -> 86 | assert False 87 | ) 88 | |> Task.map (test "Should be able to decode the document object") 89 | 90 | 91 | tests : Task () Test 92 | tests = 93 | Task.map (suite "WebAPI.DocumentTest") <| 94 | sequence 95 | [ getReadyStateTest 96 | , readyStateTest 97 | , testValue 98 | , getTitleTest, setTitleTest 99 | ] 100 | -------------------------------------------------------------------------------- /test/src/elm/WebAPI/Event/CustomEventTest.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.Event.CustomEventTest where 2 | 3 | import ElmTest.Assertion exposing (..) 4 | import ElmTest.Test exposing (..) 5 | 6 | import Task exposing (Task, andThen, sequence, map) 7 | import Json.Decode as JD exposing ((:=)) 8 | import Json.Encode as JE 9 | 10 | import TestUtil exposing (sample) 11 | 12 | import WebAPI.Window as Window 13 | import WebAPI.Document as Document 14 | import WebAPI.Event as Event 15 | import WebAPI.Event.Custom as CustomEvent 16 | import WebAPI.Date 17 | 18 | 19 | and = flip Task.andThen 20 | andAlways = and << always 21 | recover = flip Task.onError 22 | 23 | 24 | testListenerWithDetail : Task x Test 25 | testListenerWithDetail = 26 | let 27 | mbox = 28 | Signal.mailbox (JE.int 0) 29 | 30 | responder event listener = 31 | [ Event.send (Signal.message mbox.address (CustomEvent.detail event)) 32 | , Event.remove 33 | ] 34 | 35 | select = 36 | CustomEvent.select "myownevent" 37 | 38 | in 39 | Event.on select responder Window.target 40 | |> andAlways (Event.construct select (CustomEvent.options (JE.int 17) Event.defaultOptions)) 41 | |> and (CustomEvent.toEvent >> Event.dispatch Window.target) 42 | |> andAlways (Task.sleep 5) 43 | |> andAlways (sample mbox.signal) 44 | |> Task.map (assertEqual (Result.Ok 17) << JD.decodeValue JD.int) 45 | |> recover (Task.succeed << assertEqual "no error") 46 | |> Task.map (test "listening with detail should work") 47 | 48 | 49 | testToEvent : Task x Test 50 | testToEvent = 51 | Event.construct (CustomEvent.select "anevent") (CustomEvent.options (JE.int 17) Event.defaultOptions) 52 | |> Task.map (CustomEvent.toEvent >> Event.eventType) 53 | |> Task.map (assertEqual "anevent") 54 | |> Task.map (test "testToEvent") 55 | 56 | 57 | testFromEventGood : Task x Test 58 | testFromEventGood = 59 | Event.construct (CustomEvent.select "anevent") (CustomEvent.options (JE.int 17) Event.defaultOptions) 60 | |> Task.map (CustomEvent.toEvent >> CustomEvent.fromEvent) 61 | |> Task.map (Maybe.map (CustomEvent.detail >> (JD.decodeValue JD.int))) 62 | |> Task.map (assertEqual (Just (Result.Ok 17))) 63 | |> Task.map (test "testFromEventGood") 64 | 65 | 66 | testFromEventBad : Task x Test 67 | testFromEventBad = 68 | Event.construct (Event.select "anevent") Event.defaultOptions 69 | |> Task.map (CustomEvent.fromEvent) 70 | |> Task.map (assertEqual Nothing) 71 | |> Task.map (test "testFromEventBad") 72 | 73 | 74 | testDecoderGood : Task x Test 75 | testDecoderGood = 76 | Event.construct (CustomEvent.select "myownevent") (CustomEvent.options (JE.int 17) Event.defaultOptions) 77 | |> Task.map (JD.decodeValue CustomEvent.decoder << CustomEvent.encode) 78 | |> Task.map (\result -> case result of 79 | Ok _ -> assert True 80 | Err _ -> assert False 81 | ) 82 | |> Task.map (test "testDecoderGood") 83 | 84 | 85 | testEventDecoder : Task x Test 86 | testEventDecoder = 87 | Event.construct (CustomEvent.select "myownevent") (CustomEvent.options (JE.int 17) Event.defaultOptions) 88 | |> Task.map (JD.decodeValue Event.decoder << CustomEvent.encode) 89 | |> Task.map (\result -> case result of 90 | Ok _ -> assert True 91 | Err _ -> assert False 92 | ) 93 | |> Task.map (test "testEventDecoder") 94 | 95 | 96 | testDecoderBad : Task x Test 97 | testDecoderBad = 98 | Task.map (test "testDecoderBad") <| 99 | Task.succeed <| 100 | case JD.decodeValue CustomEvent.decoder JE.null of 101 | Ok _ -> assert False 102 | Err _ -> assert True 103 | 104 | 105 | tests : Task () Test 106 | tests = 107 | Task.map (suite "WebAPI.Event.CustomEventTest") <| 108 | sequence 109 | [ testListenerWithDetail 110 | , testToEvent 111 | , testFromEventGood, testFromEventBad 112 | , testDecoderGood, testEventDecoder, testDecoderBad 113 | ] 114 | -------------------------------------------------------------------------------- /test/src/elm/WebAPI/LocationTest.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.LocationTest where 2 | 3 | import ElmTest.Test exposing (..) 4 | import ElmTest.Assertion exposing (..) 5 | import Task exposing (Task, sequence, succeed, andThen) 6 | import String 7 | 8 | import WebAPI.Location exposing (..) 9 | 10 | 11 | locationTest : Task () Test 12 | locationTest = 13 | location |> 14 | Task.map (\loc -> 15 | suite "location" 16 | [ test "hash" <| assertEqual "" loc.hash 17 | , test "host" <| assertEqual "localhost:8080" loc.host 18 | , test "hostname" <| assertEqual "localhost" loc.hostname 19 | , test "href" <| 20 | assert <| 21 | List.all identity 22 | [ String.startsWith "http://" loc.href 23 | , String.endsWith "elm.html" loc.href 24 | ] 25 | , test "origin" <| assertEqual "http://localhost:8080" loc.origin 26 | , test "pathname" <| 27 | assert <| 28 | List.all identity 29 | [ String.startsWith "/" loc.pathname 30 | , String.endsWith "elm.html" loc.pathname 31 | ] 32 | , test "port'" <| assertEqual "8080" loc.port' 33 | , test "protocol" <| assertEqual "http:" loc.protocol 34 | , test "search" <| assertEqual "" loc.search 35 | ] 36 | ) 37 | 38 | 39 | tests : Task () Test 40 | tests = 41 | Task.map (suite "WebAPI.LocationTest") <| 42 | sequence <| 43 | [ locationTest 44 | ] 45 | 46 | -------------------------------------------------------------------------------- /test/src/elm/WebAPI/MathTest.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.MathTest where 2 | 3 | import ElmTest.Test exposing (..) 4 | import ElmTest.Assertion exposing (..) 5 | import Task exposing (Task, sequence, succeed) 6 | 7 | import WebAPI.Math 8 | 9 | 10 | within : Float -> Float -> Float -> Assertion 11 | within tolerance value1 value2 = 12 | assert <| 13 | abs (value1 - value2) < tolerance 14 | 15 | 16 | within001 : Float -> Float -> Assertion 17 | within001 = within 0.001 18 | 19 | 20 | random : Task x Test 21 | random = 22 | WebAPI.Math.random |> 23 | Task.map (\r -> 24 | test "random" <| 25 | assert <| 26 | (r >= 0 && r <= 1) 27 | ) 28 | 29 | 30 | tests : Task x Test 31 | tests = 32 | Task.map (suite "WebAPI.Math") <| 33 | sequence <| 34 | List.map succeed 35 | [ test "ln2" <| within001 WebAPI.Math.ln2 0.693 36 | , test "ln10" <| within001 WebAPI.Math.ln10 2.303 37 | , test "log2e" <| within001 WebAPI.Math.log2e 1.443 38 | , test "log10e" <| within001 WebAPI.Math.log10e 0.434 39 | , test "sqrt1_2" <| within001 WebAPI.Math.sqrt1_2 0.707 40 | , test "sqrt2" <| within001 WebAPI.Math.sqrt2 1.414 41 | , test "exp" <| within001 (WebAPI.Math.exp 2) (e ^ 2) 42 | , test "log" <| within001 (WebAPI.Math.log 27) (logBase e 27) 43 | ] 44 | ++ 45 | [ random 46 | ] 47 | -------------------------------------------------------------------------------- /test/src/elm/WebAPI/NumberTest.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.NumberTest where 2 | 3 | import ElmTest.Test exposing (..) 4 | import ElmTest.Assertion exposing (..) 5 | import Task exposing (Task, sequence, succeed) 6 | import Result exposing (Result(..)) 7 | 8 | import WebAPI.Number 9 | 10 | 11 | within : Float -> Float -> Float -> Assertion 12 | within tolerance value1 value2 = 13 | assert <| 14 | abs (value1 - value2) < tolerance 15 | 16 | 17 | within001 : Float -> Float -> Assertion 18 | within001 = within 0.001 19 | 20 | 21 | isErr : Result a b -> Bool 22 | isErr result = 23 | case result of 24 | Ok _ -> False 25 | Err _ -> True 26 | 27 | 28 | toFixedDigitsFailure : Test 29 | toFixedDigitsFailure = 30 | let 31 | result = 32 | WebAPI.Number.toFixedDigits -10 200 33 | 34 | in 35 | -- Firefox gives 0 for this, which I suppose is somewhat sensible 36 | test ("toFixedDigits -10 200 should fail, or be 0: " ++ (toString result)) <| 37 | assert <| 38 | (isErr result) || result == (Ok "0") 39 | 40 | 41 | tests : Task x Test 42 | tests = 43 | Task.map (suite "WebAPI.Number") <| 44 | sequence <| 45 | List.map succeed 46 | [ test "maxValue" <| assert <| WebAPI.Number.maxValue > 1000 47 | , test "minValue" <| within001 WebAPI.Number.minValue 0 48 | , test "nan" <| assert <| isNaN WebAPI.Number.nan 49 | , test "negativeInfinity" <| assert <| isInfinite WebAPI.Number.negativeInfinity 50 | , test "positiveInfinity" <| assert <| isInfinite WebAPI.Number.positiveInfinity 51 | , test "toExponential" <| assertEqual (WebAPI.Number.toExponential 200) "2e+2" 52 | , test "toExponentialDigits success" <| assertEqual (WebAPI.Number.toExponentialDigits 1 200.0) (Ok "2.0e+2") 53 | , test "toExponentialDigits failure" <| assert <| isErr (WebAPI.Number.toExponentialDigits -10 200) 54 | , test "toExponentialDigits integer" <| assertEqual (WebAPI.Number.toExponentialDigits 1 200) (Ok "2.0e+2") 55 | , test "safeExponentialDigits success" <| assertEqual (WebAPI.Number.safeExponentialDigits 1 200.0) "2.0e+2" 56 | , test "safeExponentialDigits failure" <| assertEqual (WebAPI.Number.safeExponentialDigits -10 200.0) "2e+2" 57 | , test "toFixed" <| assertEqual (WebAPI.Number.toFixed 200.1) "200" 58 | , test "toFixedDigits success" <| assertEqual (WebAPI.Number.toFixedDigits 2 200.1) (Ok "200.10") 59 | , toFixedDigitsFailure 60 | , test "toFixedDigits integer" <| assertEqual (WebAPI.Number.toFixedDigits 2 200) (Ok "200.00") 61 | , test "safeFixedDigits success" <| assertEqual (WebAPI.Number.safeFixedDigits 2 200.1) "200.10" 62 | , test "safeFixedDigits failure" <| assertEqual (WebAPI.Number.safeFixedDigits -10 200.1) "200" 63 | , test "toPrecisionDigits success" <| assertEqual (WebAPI.Number.toPrecisionDigits 5 200.1) (Ok "200.10") 64 | , test "toPrecisionDigits failure" <| assert <| isErr (WebAPI.Number.toPrecisionDigits -10 200) 65 | , test "toPrecisionDigits integer" <| assertEqual (WebAPI.Number.toPrecisionDigits 2 223) (Ok "2.2e+2") 66 | , test "safePrecisionDigits success" <| assertEqual (WebAPI.Number.safePrecisionDigits 5 200.1) "200.10" 67 | , test "safePrecisionDigits failure" <| assertEqual (WebAPI.Number.safePrecisionDigits -10 200.1) "2e+2" 68 | , test "toStringUsingBase success" <| assertEqual (WebAPI.Number.toStringUsingBase 16 32.0) (Ok "20") 69 | , test "toStringUsingBase failure" <| assert <| isErr (WebAPI.Number.toStringUsingBase -10 200) 70 | , test "toStringUsingBase integer" <| assertEqual (WebAPI.Number.toStringUsingBase 16 32) (Ok "20") 71 | , test "safeStringUsingBase success" <| assertEqual (WebAPI.Number.safeStringUsingBase 16 32.0) "20" 72 | , test "safeStringUsingBase failure" <| assertEqual (WebAPI.Number.safeStringUsingBase -10 32) "100000" 73 | ] 74 | -------------------------------------------------------------------------------- /test/src/elm/WebAPI/ScreenTest.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.ScreenTest where 2 | 3 | import ElmTest.Test exposing (..) 4 | import ElmTest.Assertion exposing (..) 5 | import Task exposing (Task, sequence, succeed, andThen) 6 | import String 7 | 8 | import WebAPI.Screen exposing (..) 9 | 10 | 11 | screenTest : Task () Test 12 | screenTest = 13 | screen |> 14 | Task.map (\s -> 15 | test "screen" << 16 | assert <| 17 | List.all ((flip (>=)) 0) <| 18 | List.map ((|>) s) 19 | [ .availTop 20 | , .availLeft 21 | , .availHeight 22 | , .availWidth 23 | , .colorDepth 24 | , .pixelDepth 25 | , .height 26 | , .width 27 | ] 28 | ) 29 | 30 | 31 | screenXYTest : Task () Test 32 | screenXYTest = 33 | screenXY |> 34 | Task.map (\(x, y) -> 35 | test 36 | (String.join "" 37 | [ "screenXY (" 38 | , toString x, ", " 39 | , toString y, ")" 40 | ] 41 | ) <| 42 | -- Note that in IE, you get (-8, -8), which I suppose actually 43 | -- is meaningful. 44 | assert (x >= -32 && y >= -32) 45 | ) 46 | 47 | 48 | tests : Task () Test 49 | tests = 50 | Task.map (suite "WebAPI.ScreenTest") <| 51 | sequence <| 52 | [ screenTest 53 | , screenXYTest 54 | ] 55 | 56 | -------------------------------------------------------------------------------- /test/src/elm/WebAPI/StorageTest.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.StorageTest where 2 | 3 | import ElmTest.Test exposing (..) 4 | import ElmTest.Assertion exposing (..) 5 | import Task exposing (Task, sequence, succeed, andThen) 6 | 7 | import WebAPI.Storage exposing (..) 8 | 9 | 10 | (>>>) = flip Task.map 11 | (>>+) = Task.andThen 12 | (>>!) = Task.onError 13 | 14 | 15 | (>>-) task func = 16 | task `andThen` (always func) 17 | 18 | 19 | reportError : Bool -> String -> Error -> Task x Test 20 | reportError disabled name error = 21 | Task.succeed <| 22 | test (name ++ ": " ++ (toString error)) <| 23 | -- If we're disabled, then we expected an error ... 24 | -- so, that's good. If not, then we didn't. 25 | if disabled 26 | then assertEqual error Disabled 27 | else assert False 28 | 29 | 30 | length0Test : Bool -> Storage -> Task x Test 31 | length0Test disabled storage = 32 | clear storage 33 | >>- length storage 34 | >>> (assertEqual 0 >> test "length0") 35 | >>! reportError disabled "length0" 36 | 37 | 38 | length1Test : Bool -> Storage -> Task x Test 39 | length1Test disabled storage = 40 | clear storage 41 | >>- set storage "bob" "joe" 42 | >>- length storage 43 | >>> (assertEqual 1 >> test "length1") 44 | >>! reportError disabled "length1" 45 | 46 | 47 | keyTestSuccess : Bool -> Storage -> Task x Test 48 | keyTestSuccess disabled storage = 49 | clear storage 50 | >>- set storage "bob" "joe" 51 | >>- key storage 0 52 | >>> (assertEqual (Just "bob") >> test "keySuccess") 53 | >>! reportError disabled "keySuccess" 54 | 55 | 56 | keyTestError : Bool -> Storage -> Task x Test 57 | keyTestError disabled storage = 58 | clear storage 59 | >>- set storage "bob" "joe" 60 | >>- key storage 5 61 | >>> (assertEqual Nothing >> test "keyError") 62 | >>! reportError disabled "keyError" 63 | 64 | 65 | getItemTestSuccess : Bool -> Storage -> Task x Test 66 | getItemTestSuccess disabled storage = 67 | clear storage 68 | >>- set storage "bob" "joe" 69 | >>- get storage "bob" 70 | >>> (assertEqual (Just "joe") >> test "getItemSuccess") 71 | >>! reportError disabled "getItemSuccess" 72 | 73 | 74 | getItemTestError : Bool -> Storage -> Task x Test 75 | getItemTestError disabled storage = 76 | clear storage 77 | >>- set storage "bob" "joe" 78 | >>- get storage "wrong" 79 | >>> (assertEqual Nothing >> test "getItemError") 80 | >>! reportError disabled "getItemError" 81 | 82 | 83 | removeItemTest : Bool -> Storage -> Task x Test 84 | removeItemTest disabled storage = 85 | clear storage 86 | >>- set storage "bob" "joe" 87 | >>- remove storage "bob" 88 | >>- length storage 89 | >>> (assertEqual 0 >> test "removeItem") 90 | >>! reportError disabled "removeItem" 91 | 92 | 93 | removeItemTestError : Bool -> Storage -> Task x Test 94 | removeItemTestError disabled storage = 95 | clear storage 96 | >>- set storage "bob" "joe" 97 | >>- remove storage "not there" 98 | >>- length storage 99 | >>> (assertEqual 1 >> test "removeItemError") 100 | >>! reportError disabled "removeItemError" 101 | 102 | 103 | enabledTest : Bool -> Task x Test 104 | enabledTest disabled = 105 | enabled 106 | >>> (assertEqual (not disabled) >> test "Enabled") 107 | 108 | 109 | tests : Bool -> Task x Test 110 | tests disabled = 111 | Task.map (suite "Storage") <| 112 | sequence <| 113 | List.map (makeSuite disabled) 114 | [ (local, "localStorage") 115 | , (session, "sessionStorage") 116 | ] 117 | ++ 118 | [ enabledTest disabled ] 119 | 120 | 121 | makeSuite : Bool -> (Storage, String) -> Task x Test 122 | makeSuite disabled (storage, label) = 123 | Task.map (suite label) <| 124 | sequence 125 | [ length0Test disabled storage 126 | , length1Test disabled storage 127 | , keyTestSuccess disabled storage 128 | , keyTestError disabled storage 129 | , getItemTestSuccess disabled storage 130 | , getItemTestError disabled storage 131 | , removeItemTest disabled storage 132 | , removeItemTestError disabled storage 133 | ] 134 | -------------------------------------------------------------------------------- /test/src/elm/WebAPI/WindowTest.elm: -------------------------------------------------------------------------------- 1 | module WebAPI.WindowTest where 2 | 3 | import ElmTest.Assertion exposing (..) 4 | import ElmTest.Test exposing (..) 5 | import ElmTest.Runner.Element exposing (runDisplay) 6 | 7 | import Task exposing (Task, andThen, sequence, map) 8 | import Json.Decode as JD 9 | 10 | import WebAPI.Window as Window 11 | 12 | 13 | (>>>) = flip Task.map 14 | (>>+) = Task.andThen 15 | 16 | 17 | isOnline : Task x Test 18 | isOnline = 19 | Window.isOnline >>> (assert >> test "isOnline should be true") 20 | 21 | 22 | testValue : Task x Test 23 | testValue = 24 | let 25 | mathPiDecoder = 26 | JD.at ["Math", "PI"] JD.float 27 | 28 | in 29 | Window.value 30 | |> Task.map (JD.decodeValue mathPiDecoder) 31 | |> Task.map (\result -> 32 | case result of 33 | Ok float -> 34 | assertEqual Basics.pi float 35 | 36 | Err _ -> 37 | assert False 38 | ) 39 | |> Task.map (test "Should be able to decode the window object") 40 | 41 | 42 | tests : Task () Test 43 | tests = 44 | Task.map (suite "WebAPI.WindowTest") <| 45 | sequence 46 | [ isOnline 47 | , testValue 48 | ] 49 | -------------------------------------------------------------------------------- /test/src/mocha/browser.js: -------------------------------------------------------------------------------- 1 | var windowTest = require('./windowTest'); 2 | var elmTest = require('./elmTest'); 3 | var locationTest = require('./locationTest'); 4 | var storageTest = require('./storageTest'); 5 | 6 | module.exports = function (browser) { 7 | var title = 8 | browser.desiredCapabilities.browserName + "-" + 9 | browser.desiredCapabilities.version + "-" + 10 | browser.desiredCapabilities.platform + " " + 11 | browser.desiredCapabilities.build; 12 | 13 | describe(title, function () { 14 | this.timeout(900000); 15 | this.slow(4000); 16 | 17 | var allPassed = true; 18 | 19 | // Before any tests run, initialize the browser. 20 | before(function (done) { 21 | browser.init(function (err) { 22 | if (err) throw err; 23 | done(); 24 | }); 25 | }); 26 | 27 | elmTest(browser); 28 | windowTest(browser); 29 | locationTest(browser); 30 | storageTest(browser); 31 | 32 | afterEach(function() { 33 | allPassed = allPassed && (this.currentTest.state === 'passed'); 34 | }); 35 | 36 | after(function (done) { 37 | console.log(title + (allPassed ? " PASSED" : " FAILED")); 38 | browser.passed(allPassed, done); 39 | }); 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /test/src/mocha/elmTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var count = require('count-substring'); 3 | var Q = require('q'); 4 | var coverage = require('../coverage'); 5 | 6 | module.exports = function (browser) { 7 | var url; 8 | 9 | // Test for false, because null should default to true 10 | if (browser.desiredCapabilities.webStorageEnabled === false) { 11 | url = 'http://localhost:8080/build/elm-disable-storage.html'; 12 | } else if (browser.desiredCapabilities.rafEnabled === false) { 13 | url = 'http://localhost:8080/build/elm-disable-raf.html'; 14 | } else { 15 | url = 'http://localhost:8080/build/elm.html'; 16 | } 17 | 18 | describe("The tests written in Elm", function () { 19 | var falsy = function () { 20 | return Q.when(false); 21 | }; 22 | 23 | it('should pass', function () { 24 | return browser 25 | .url(url) 26 | .waitUntil(function () { 27 | return this.getText("#results").then(function (text) { 28 | return text.indexOf("suites run") > 0; 29 | }, falsy); 30 | }, 30000, 500) 31 | .getText("#results") 32 | .then(function (text) { 33 | // Always log the test results 34 | console.log(text); 35 | 36 | var failedCount = count(text, "FAILED"); 37 | expect(failedCount).to.equal(0); 38 | }); 39 | }); 40 | 41 | afterEach(function () { 42 | return coverage.collect(browser); 43 | }); 44 | }); 45 | }; 46 | 47 | -------------------------------------------------------------------------------- /test/src/mocha/locationTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var Q = require('q'); 3 | var coverage = require('../coverage'); 4 | 5 | module.exports = function (browser) { 6 | // Test for false, because null should default to true 7 | if ( 8 | browser.desiredCapabilities.webStorageEnabled === false || 9 | browser.desiredCapabilities.rafEnabled === false 10 | ) return; 11 | 12 | describe("The Location example", function () { 13 | beforeEach(function (done) { 14 | browser.url('http://localhost:8080/build/location.html', done); 15 | }); 16 | 17 | // This won't really work, because we've reloaded ... 18 | afterEach(function () { 19 | return coverage.collect(browser); 20 | }); 21 | 22 | var falsy = function () { 23 | return Q.when(false); 24 | }; 25 | 26 | it("should reload from server", function () { 27 | return browser 28 | .setValue("#input", "This goes away on reload") 29 | .click("#reload-force-button") 30 | 31 | // Wait for it not to have a value again 32 | .waitUntil(function () { 33 | return this.getValue("#input").then(function (value) { 34 | return value === ""; 35 | }, falsy); 36 | }, 6000, 250); 37 | }); 38 | 39 | it("should reload from cache", function () { 40 | return browser 41 | .setValue("#input", "This goes away on reload") 42 | .click("#reload-cache-button") 43 | 44 | // Wait for it not to have a value again 45 | .waitUntil(function () { 46 | return this.getValue("#input").then(function (value) { 47 | return value === ""; 48 | }, falsy); 49 | }, 6000, 250); 50 | }); 51 | 52 | /* jshint laxbreak: true */ 53 | var runError = 54 | browser.desiredCapabilities.browserName == "firefox" 55 | ? it 56 | : it.skip; 57 | 58 | describe("assign", function () { 59 | it("should work with valid url", function () { 60 | return browser 61 | .setValue("#input", "http://localhost:8080/build/window.html") 62 | .click("#assign-button") 63 | .waitUntil(function () { 64 | return this.url().then(function (url) { 65 | return url.value == "http://localhost:8080/build/window.html"; 66 | }); 67 | }, 6000, 250); 68 | }); 69 | 70 | runError("should error with invalid url", function () { 71 | return browser 72 | .setValue("#input", "http:// www.apple.com") 73 | .click("#assign-button") 74 | .waitUntil(function () { 75 | return this.getText("#message").then(function (message) { 76 | return message.indexOf("Got error:") >= 0; 77 | }); 78 | }, 6000, 250); 79 | }); 80 | }); 81 | 82 | describe("replace", function () { 83 | it("should work with valid url", function () { 84 | return browser 85 | .setValue("#input", "http://localhost:8080/build/window.html") 86 | .click("#replace-button") 87 | .waitUntil(function () { 88 | return this.url().then(function (url) { 89 | return url.value == "http://localhost:8080/build/window.html"; 90 | }); 91 | }, 6000, 250); 92 | }); 93 | 94 | runError("should error with invalid url", function () { 95 | return browser 96 | .setValue("#input", "http:// www.apple.com") 97 | .click("#replace-button") 98 | .waitUntil(function () { 99 | return this.getText("#message").then(function (message) { 100 | return message.indexOf("Got error:") >= 0; 101 | }); 102 | }, 6000, 250); 103 | }); 104 | }); 105 | }); 106 | }; 107 | -------------------------------------------------------------------------------- /test/src/mocha/storageTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var Q = require('q'); 3 | var coverage = require('../coverage'); 4 | 5 | module.exports = function (browser) { 6 | var run; 7 | 8 | if ( 9 | browser.desiredCapabilities.rafEnabled === false 10 | ) return; 11 | 12 | if ( 13 | browser.desiredCapabilities.browserName == 'chrome' || 14 | browser.desiredCapabilities.browserName == 'internet explorer' || 15 | browser.desiredCapabilities.browserName == 'opera' || 16 | browser.desiredCapabilities.browserName == 'iphone' 17 | ) { 18 | // Can't get the tab switching to work in these 19 | run = describe.skip; 20 | } else { 21 | run = describe; 22 | } 23 | 24 | // Test for false, because null should default to true 25 | if (browser.desiredCapabilities.webStorageEnabled === false) { 26 | // Skip if we've disabled web storage 27 | run = describe.skip; 28 | } 29 | 30 | var falsy = function () { 31 | return Q.when(false); 32 | }; 33 | 34 | run("The Storage example", function () { 35 | var url = 'http://localhost:8080/build/storage.html'; 36 | 37 | before(function () { 38 | return browser 39 | .newWindow(url, "tab1") 40 | .waitForExist("#select-area", 6000) 41 | .selectByIndex("#select-area", 0) 42 | .selectByIndex("#select-operation", 5) 43 | .click("#perform-action") 44 | .newWindow(url, "tab2") 45 | .waitForExist("#select-area", 6000) 46 | .selectByIndex("#select-area", 0) 47 | .selectByIndex("#select-operation", 5) 48 | .click("#perform-action"); 49 | }); 50 | 51 | after(function () { 52 | return browser 53 | .switchTab("tab1") 54 | .then(function () { 55 | return coverage.collect(browser); 56 | }) 57 | .then(function () { 58 | return browser 59 | .close() 60 | .switchTab("tab2") 61 | .then(function () { 62 | return coverage.collect(browser); 63 | }) 64 | .then(function () { 65 | return browser.close(); 66 | }); 67 | }); 68 | }); 69 | 70 | it("first set should trigger add event", function () { 71 | // These expecteText checks are a little lazy, since the actual 72 | // order of the properties is not necessarily deterministic -- it 73 | // changed between Elm 0.15 and 0.16, for instance. But, at least 74 | // if it passes, I know it's good -- it's failure that might be 75 | // mistaken. 76 | var expectedText = "LogEvent { area = Local, url = \"" + url + "\", change = Add \"testKey\" \"testValue\" }"; 77 | return browser 78 | .switchTab("tab1") 79 | .waitForExist("#select-area", 6000) 80 | .selectByIndex("#select-area", 0) 81 | .selectByIndex("#select-operation", 3) 82 | .waitForExist("#select-set-key", 6000) 83 | .setValue("#select-set-key", "testKey") 84 | .setValue("#select-set-value", "testValue") 85 | .click("#perform-action") 86 | .switchTab("tab2") 87 | .waitUntil(function () { 88 | return this.getText("#log").then(function (text) { 89 | return text.indexOf(expectedText) >= 0; 90 | }); 91 | }, 8000, 250); 92 | }); 93 | 94 | it("second set should trigger modify event", function () { 95 | var expectedText = "LogEvent { area = Local, url = \"" + url + "\", change = Modify \"testKey\" \"testValue\" \"testValue2\" }"; 96 | 97 | return browser 98 | .switchTab("tab1") 99 | .setValue("#select-set-key", "testKey") 100 | .setValue("#select-set-value", "testValue2") 101 | .click("#perform-action") 102 | .switchTab("tab2") 103 | .waitUntil(function () { 104 | return this.getText("#log").then(function (text) { 105 | return text.indexOf(expectedText) >= 0; 106 | }); 107 | }, 8000, 250); 108 | }); 109 | 110 | it("remove should trigger remove event", function () { 111 | var expectedText = "LogEvent { area = Local, url = \"" + url + "\", change = Remove \"testKey\" \"testValue2\" }"; 112 | 113 | return browser 114 | .switchTab("tab1") 115 | .selectByIndex("#select-operation", 4) 116 | .waitForExist("#select-remove-key", 6000) 117 | .setValue("#select-remove-key", "testKey") 118 | .click("#perform-action") 119 | .switchTab("tab2") 120 | .waitUntil(function () { 121 | return this.getText("#log").then(function (text) { 122 | return text.indexOf(expectedText) >= 0; 123 | }); 124 | }, 8000, 250); 125 | }); 126 | 127 | it("clear should trigger clear event", function () { 128 | var expectedText = "LogEvent { area = Local, url = \"" + url + "\", change = Clear }"; 129 | 130 | return browser 131 | .switchTab("tab1") 132 | .selectByIndex("#select-operation", 3) 133 | .waitForExist("#select-set-key", 6000) 134 | .setValue("#select-set-key", "testKey") 135 | .setValue("#select-set-value", "testValue") 136 | .click("#perform-action") 137 | .selectByIndex("#select-operation", 5) 138 | .click("#perform-action") 139 | .switchTab("tab2") 140 | .waitUntil(function () { 141 | return browser.getHTML("#log").then(function (text) { 142 | return text.indexOf(expectedText) >= 0; 143 | }, falsy); 144 | }, 8000, 250); 145 | }); 146 | }); 147 | }; 148 | -------------------------------------------------------------------------------- /test/src/mocha/windowTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var Q = require('q'); 3 | var coverage = require('../coverage'); 4 | 5 | module.exports = function (browser) { 6 | // Test for false, because null should default to true 7 | if ( 8 | browser.desiredCapabilities.webStorageEnabled === false || 9 | browser.desiredCapabilities.rafEnabled === false 10 | ) return; 11 | 12 | describe("The Window example", function () { 13 | beforeEach(function () { 14 | return browser.url('http://localhost:8080/build/window.html'); 15 | }); 16 | 17 | afterEach(function () { 18 | return coverage.collect(browser); 19 | }); 20 | 21 | // Don't test alerts etc. under Safari, because Selenium can't 22 | // manage alerts with Safari. 23 | var describeAlert = ( 24 | browser.desiredCapabilities.browserName == 'safari' || 25 | browser.desiredCapabilities.browserName == 'opera' 26 | ) ? describe.skip : describe; 27 | 28 | var truthy = function () { 29 | return Q.when(true); 30 | }; 31 | 32 | var falsy = function () { 33 | return Q.when(false); 34 | }; 35 | 36 | describeAlert("alert", function () { 37 | it("should open", function () { 38 | return browser 39 | .click("#alert-button") 40 | 41 | .waitUntil(function () { 42 | return this.alertText().then(truthy, falsy); 43 | }, 30000, 500) 44 | 45 | .alertText().then(function (text) { 46 | expect(text).to.equal("Hello world!"); 47 | }) 48 | 49 | .alertAccept(); 50 | }); 51 | }); 52 | 53 | describeAlert("confirm", function () { 54 | it("should recognize acceptance", function () { 55 | return browser 56 | .click("#confirm-button") 57 | 58 | .waitUntil(function () { 59 | return this.alertText().then(truthy, falsy); 60 | }, 30000, 500) 61 | 62 | .alertText().then(function (text) { 63 | expect(text).to.equal("Do you agree?"); 64 | }) 65 | 66 | .alertAccept() 67 | 68 | .waitUntil(function () { 69 | return this.getText("#message").then(function (text) { 70 | return text.indexOf("Pressed OK") >= 0; 71 | }); 72 | }, 30000, 500); 73 | }); 74 | 75 | it("should recognize rejection", function () { 76 | return browser 77 | .click("#confirm-button") 78 | 79 | .waitUntil(function () { 80 | return this.alertText().then(truthy, falsy); 81 | }, 30000, 500) 82 | 83 | .alertText().then(function (text) { 84 | expect(text).to.equal("Do you agree?"); 85 | }) 86 | 87 | .alertDismiss() 88 | 89 | .waitUntil(function () { 90 | return this.getText("#message").then(function (text) { 91 | return text.indexOf("Pressed cancel") >= 0; 92 | }); 93 | }, 30000, 500); 94 | }); 95 | }); 96 | 97 | describeAlert("prompt", function () { 98 | it("should recognize dismissal", function () { 99 | return browser 100 | .click("#prompt-button") 101 | 102 | .waitUntil(function () { 103 | return this.alertText().then(truthy, falsy); 104 | }, 30000, 500) 105 | 106 | .alertText().then(function (text) { 107 | expect(text).to.equal("What is your favourite colour?"); 108 | }) 109 | 110 | .alertDismiss() 111 | 112 | .waitUntil(function () { 113 | return this.getText("#message").then(function (text) { 114 | return text.indexOf("User canceled.") >= 0; 115 | }); 116 | }, 30000, 500); 117 | }); 118 | 119 | it("should return default when accepted", function () { 120 | return browser 121 | .click("#prompt-button") 122 | 123 | .waitUntil(function () { 124 | return this.alertText().then(truthy, falsy); 125 | }, 30000, 500) 126 | 127 | .alertText().then(function (text) { 128 | expect(text).to.equal("What is your favourite colour?"); 129 | }) 130 | 131 | .alertAccept() 132 | 133 | .waitUntil(function () { 134 | return this.getText("#message").then(function (text) { 135 | return text.indexOf("Got response: Blue") >= 0; 136 | }); 137 | }, 30000, 500); 138 | }); 139 | 140 | // These don't work on iPhone because the alertText() isn't cleared 141 | var run = browser.desiredCapabilities.browserName == 'iphone' ? it.skip : it; 142 | 143 | run("should interpret empty string as dismissal", function () { 144 | return browser 145 | .click("#prompt-button") 146 | 147 | .waitUntil(function () { 148 | return this.alertText().then(truthy, falsy); 149 | }, 30000, 500) 150 | 151 | .alertText("") 152 | .alertAccept() 153 | 154 | .waitUntil(function () { 155 | return this.getText("#message").then(function (text) { 156 | return text.indexOf("User canceled.") >= 0; 157 | }); 158 | }, 30000, 500); 159 | }); 160 | 161 | run("should return entered text if entered", function () { 162 | return browser 163 | .click("#prompt-button") 164 | 165 | .waitUntil(function () { 166 | return this.alertText().then(truthy, falsy); 167 | }, 30000, 500) 168 | 169 | .alertText("Red") 170 | .alertAccept() 171 | 172 | .waitUntil(function () { 173 | return this.getText("#message").then(function (text) { 174 | return text.indexOf("Got response: Red") >= 0; 175 | }); 176 | }, 30000, 500); 177 | }); 178 | }); 179 | }); 180 | }; 181 | -------------------------------------------------------------------------------- /test/src/remote.js: -------------------------------------------------------------------------------- 1 | // Definition of browsers to test remotely 2 | 3 | module.exports = function remote (rev) { 4 | return [{ 5 | browserName: 'safari', 6 | version: '6.0', 7 | platform: 'OS X 10.8', 8 | build: rev, 9 | name: 'Safari Mountain Lion ' + rev 10 | },{ 11 | browserName: 'safari', 12 | version: '7.0', 13 | platform: 'OS X 10.9', 14 | build: rev, 15 | name: 'Safari Mavericks ' + rev 16 | },{ 17 | browserName: 'safari', 18 | version: '8.0', 19 | platform: 'OS X 10.10', 20 | build: rev, 21 | name: 'Safari Yosemite ' + rev 22 | },{ 23 | browserName: 'safari', 24 | version: '9.0', 25 | platform: 'OS X 10.11', 26 | build: rev, 27 | name: 'Safari El Capitan ' + rev 28 | },{ 29 | browserName: 'chrome', 30 | version: '46.0', 31 | platform: 'Windows 10', 32 | build: rev, 33 | name: 'Chrome Windows 10 46.0 ' + rev 34 | },{ 35 | browserName: 'chrome', 36 | version: '45.0', 37 | platform: 'Windows 10', 38 | build: rev, 39 | name: 'Chrome Windows 10 45.0 ' + rev 40 | },{ 41 | browserName: 'chrome', 42 | version: '44.0', 43 | platform: 'Windows 10', 44 | build: rev, 45 | name: 'Chrome Windows 10 44.0 ' + rev 46 | },{ 47 | browserName: 'chrome', 48 | version: '43.0', 49 | platform: 'Windows 10', 50 | build: rev, 51 | name: 'Chrome Windows 10 43.0 ' + rev 52 | },{ 53 | browserName: 'firefox', 54 | version: '41.0', 55 | platform: 'Linux', 56 | build: rev, 57 | name: 'Firefox Linux 41.0 ' + rev 58 | },{ 59 | browserName: 'firefox', 60 | version: '40.0', 61 | platform: 'Linux', 62 | build: rev, 63 | name: 'Firefox Linux 40.0 ' + rev 64 | },{ 65 | browserName: 'firefox', 66 | version: '39.0', 67 | platform: 'Linux', 68 | build: rev, 69 | name: 'Firefox Linux 39.0 ' + rev 70 | },{ 71 | browserName: 'firefox', 72 | version: '38.0', 73 | platform: 'Linux', 74 | build: rev, 75 | name: 'Firefox Linux 38.0 ' + rev 76 | },{ 77 | browserName: 'internet explorer', 78 | version: '9.0', 79 | platform: 'Windows 7', 80 | build: rev, 81 | name: 'Internet Explorer 9.0 ' + rev, 82 | },{ 83 | browserName: 'opera', 84 | version: '12.15', 85 | platform: 'Linux', 86 | build: rev, 87 | name: 'Opera 12.15 ' + rev, 88 | },{ 89 | browserName: 'internet explorer', 90 | version: '10.0', 91 | platform: 'Windows 8', 92 | build: rev, 93 | name: 'Internet Explorer 10.0 ' + rev 94 | },{ 95 | browserName: 'internet explorer', 96 | version: '11.0', 97 | platform: 'Windows 10', 98 | build: rev, 99 | name: 'Internet Explorer 11.0 ' + rev 100 | }]; 101 | }; 102 | -------------------------------------------------------------------------------- /test/src/run.js: -------------------------------------------------------------------------------- 1 | var SeSauce = require('./selenium-sauce'); 2 | var git = require('git-rev'); 3 | 4 | var remote = require('./remote'); 5 | var config = require('./config'); 6 | var eachBrowser = require('./mocha/browser'); 7 | 8 | git.short(function (rev) { 9 | // If SauceLabs environment variables are present, set up SauceLabs browsers 10 | if (config.webdriver.user) { 11 | config.webdriver.desiredCapabilities = remote(rev); 12 | } else { 13 | config.webdriver.desiredCapabilities = [{ 14 | browserName: 'firefox', 15 | webStorageEnabled: true 16 | },{ 17 | browserName: 'firefox', 18 | webStorageEnabled: false 19 | },{ 20 | browserName: 'firefox', 21 | rafEnabled: false 22 | },{ 23 | browserName: 'chrome', 24 | webStorageEnabled: true 25 | },{ 26 | browserName: 'chrome', 27 | rafEnabled: false 28 | },{ 29 | browserName: 'chrome', 30 | webStorageEnabled: false, 31 | chromeOptions: { 32 | args: ["--disable-local-storage"] 33 | } 34 | }]; 35 | } 36 | 37 | new SeSauce(config, eachBrowser); 38 | 39 | // Need to call mocha with a --delay, since git.short is async 40 | run(); 41 | }); 42 | 43 | -------------------------------------------------------------------------------- /test/src/selenium-sauce.js: -------------------------------------------------------------------------------- 1 | var webdriverio = require('webdriverio'), 2 | httpserver = require('http-server'), 3 | selenium = require('selenium-standalone'), 4 | sauceConnectLauncher = require('sauce-connect-launcher'), 5 | extend = require('extend'), 6 | colors = require('colors'), 7 | coverage = require('./coverage'), 8 | SauceLabs = require('saucelabs'); 9 | 10 | /** 11 | * Initializes Selenium Sauce using the specified options. 12 | * 'doEachBrowser' is called once for each browser in options.webdriver.desiredCapabilities, passing in the webdriverio instance. 13 | */ 14 | var SeSauce = function(options, doEachBrowser) { 15 | 16 | extend(this, { 17 | browsers: [], // Contains a list of webdriverio instances 18 | _browserActions: [], 19 | 20 | _initialized: false, 21 | _stopped: false, 22 | 23 | options: { 24 | quiet: false, // Silences the console output 25 | webdriver: { // Options for selenium webdriver (webdriverio) 26 | host: 'ondemand.saucelabs.com', 27 | port: 80, 28 | user: null, 29 | key: null, 30 | logLevel: 'silent', 31 | desiredCapabilities: [] // Non-standard option; An array of desired capabilities instead of a single object 32 | }, 33 | httpServer: { // Options for local http server (npmjs.org/package/http-server) 34 | disable: false, // Non-standard option; used to skip launching the http server 35 | port: 8080 // Non-standard option; it is passed into the httpServer.listen() method 36 | }, 37 | sauceLabs: { // Options for SauceLabs API wrapper (npmjs.org/package/saucelabs) 38 | username: null, 39 | password: null 40 | }, 41 | sauceConnect: { // Options for SauceLabs Connect (npmjs.org/package/sauce-connect-launcher) 42 | disable: false, // Non-standard option; used to disable sauce connect 43 | username: null, 44 | accessKey: null 45 | }, 46 | selenium: { // Options for Selenium Server (npmjs.org/package/selenium-standalone). Only used if you need Selenium running locally. 47 | args: [] // options to pass to `java -jar selenium-server-standalone-X.XX.X.jar` 48 | } 49 | } 50 | }); 51 | 52 | 53 | this._doEachBrowser = doEachBrowser; 54 | this.options.quiet = options.quiet; 55 | 56 | extend(this.options.webdriver, options.webdriver || {}); 57 | extend(this.options.httpServer, options.httpServer || {}); 58 | extend(this.options.sauceLabs, options.sauceLabs || {}); 59 | extend(this.options.sauceConnect, options.sauceConnect || {}); 60 | extend(this.options.selenium, options.selenium || {}); 61 | 62 | if (this.options.webdriver.desiredCapabilities && this.options.webdriver.desiredCapabilities.constructor === Object) 63 | this.options.webdriver.desiredCapabilities = [this.options.webdriver.desiredCapabilities]; 64 | 65 | if (!(this.options.webdriver.user && this.options.webdriver.key) && this.options.webdriver.host == 'ondemand.saucelabs.com') { 66 | this.options.webdriver.host = 'localhost'; 67 | this.options.webdriver.port = 4444; 68 | } 69 | 70 | var self = this; 71 | 72 | function init (complete) { 73 | self._initOnce(function (err) { 74 | if (err) 75 | return complete(err); 76 | this._oldInit(complete); 77 | }.bind(this)); 78 | } 79 | 80 | function end (complete) { 81 | this._oldEnd(function () { 82 | self.browsers.splice(self.browsers.indexOf(this), 1); 83 | if (self.browsers.length === 0) 84 | self._stop(complete); 85 | else 86 | complete(); 87 | }.bind(this)); 88 | } 89 | 90 | function succeed (success, complete) { 91 | this.updateJob({ passed: success }, function() { 92 | this.end(complete); 93 | }.bind(this)); 94 | } 95 | 96 | function updateJob (data, complete) { 97 | if (self.sauceLabs) 98 | self.sauceLabs.updateJob(this.requestHandler.sessionID, data, complete); 99 | else 100 | complete(); 101 | } 102 | 103 | for (var i = 0, len = this.options.webdriver.desiredCapabilities.length; i < len; i++) { 104 | // Stop looping through configurations if we've errored 105 | if (this._stopped) break; 106 | 107 | var wdOptions = extend({}, this.options.webdriver); 108 | wdOptions.desiredCapabilities = this.options.webdriver.desiredCapabilities[i]; 109 | var browser = webdriverio.remote(wdOptions); 110 | this.browsers.push(browser); 111 | 112 | browser._oldInit = browser.init; 113 | browser.init = init.bind(browser); 114 | 115 | browser._oldEnd = browser.end; 116 | browser.end = end.bind(browser); 117 | 118 | browser.passed = succeed.bind(browser); 119 | browser.updateJob = updateJob.bind(browser); 120 | 121 | doEachBrowser.call(this, browser); 122 | } 123 | }; 124 | 125 | extend(SeSauce.prototype, { 126 | 127 | /** 128 | * Performs one-time initialization. Calls 'complete' when done, passing in an error message if necessary. 129 | * @private 130 | */ 131 | _initOnce: function (complete) { 132 | if (this._initialized) 133 | return complete(); 134 | 135 | var self = this; 136 | this._initialized = true; 137 | 138 | this.webdriver = webdriverio; 139 | 140 | if (!this.options.httpServer.disable) { 141 | this._log("Launching local web server (http://localhost:" + this.options.httpServer.port + "/)..."); 142 | this.httpServer = httpserver.createServer(this.options.httpServer); 143 | this.httpServer.listen(this.options.httpServer.port); 144 | this._log("Web server ready."); 145 | } 146 | 147 | if (this.options.sauceLabs.username && this.options.sauceLabs.password) { 148 | this._log("Initializing SauceLabs API."); 149 | this.sauceLabs = new SauceLabs({ 150 | username: this.options.sauceLabs.username, 151 | password: this.options.sauceLabs.password 152 | }); 153 | } 154 | 155 | if (this.options.sauceConnect.username && this.options.sauceConnect.accessKey) { 156 | if (this.options.sauceConnect.disable) 157 | this._log("Sauce Connect disabled."); 158 | else { 159 | this._log("Launching Sauce Connect..."); 160 | delete this.options.sauceConnect.disable; 161 | sauceConnectLauncher(this.options.sauceConnect, function (errmsg, process) { 162 | if (errmsg) { 163 | if (process) process.close(); 164 | return self._doError('Error launching Sauce Connect:\n' + errmsg, complete); 165 | } 166 | self.sauceConnect = process; 167 | self._log("Sauce Connect ready."); 168 | complete(); 169 | }); 170 | } 171 | } 172 | else { 173 | this._log("No SauceLabs username/accessKey. Launching Selenium locally..."); 174 | 175 | selenium.install({}, function (err) { 176 | if (err) { 177 | self._doError(err, complete); 178 | } else { 179 | selenium.start({ 180 | seleniumArgs: self.options.selenium.args 181 | }, function (err, child) { 182 | if (err) { 183 | self._doError(err, complete); 184 | } else { 185 | self.selenium = child; 186 | complete(); 187 | } 188 | }); 189 | } 190 | }); 191 | } 192 | }, 193 | 194 | /** 195 | * Logs an error message, stops all services, and then calls the 'complete' callback, passing in the error message. 196 | * @private 197 | */ 198 | _doError: function (msg, complete) { 199 | this._err(msg); 200 | this._stop(function () { 201 | complete(msg); 202 | }); 203 | }, 204 | 205 | 206 | /** 207 | * @private 208 | */ 209 | _stop: function (complete) { 210 | if (this._stopped) 211 | return complete && complete(); 212 | 213 | this._stopped = true; 214 | 215 | if (this.httpServer) { 216 | this.httpServer.close(); 217 | this._log("Web server stopped."); 218 | } 219 | 220 | if (this.selenium) { 221 | this.selenium.kill(); 222 | this._log("Local Selenium server stopped."); 223 | } 224 | 225 | coverage.report(function () { 226 | if (this.sauceConnect) { 227 | var self = this; 228 | this._log("Closing Sauce Connect..."); 229 | this.sauceConnect.close(function () { 230 | self._log("Sauce Connect closed."); 231 | if (complete) 232 | complete(); 233 | }); 234 | } 235 | else if (complete) 236 | complete(); 237 | }); 238 | }, 239 | 240 | _log: function(str) { 241 | if(!this.options.quiet) 242 | console.log('SelSauce: '.blue + str); 243 | }, 244 | 245 | _err: function(str) { 246 | console.error('SelSauce: '.bgRed + str); 247 | } 248 | }); 249 | 250 | module.exports = SeSauce; 251 | --------------------------------------------------------------------------------