diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..00472fc
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,8 @@
+version: 2
+updates:
+- package-ecosystem: mix
+ directory: "/"
+ schedule:
+ interval: daily
+ time: "07:00"
+ timezone: Europe/London
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index cadde32..6d4b5c9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,6 +30,8 @@ jobs:
restore-keys: ${{ runner.os }}-mix-
- name: Install dependencies
run: mix deps.get
- - name: Run tests
- run: mix test
+ - name: Run Tests
+ run: mix coveralls.json
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v1
diff --git a/README.md b/README.md
index d20b640..73a88bb 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,46 @@
-# Stopwatch
-[![Elixir CI](https://github.com/dwyl/phoenix-liveview-stopwatch/actions/workflows/ci.yml/badge.svg)](https://github.com/dwyl/phoenix-liveview-stopwatch/actions/workflows/ci.yml)
+
+# `Phoenix` `LiveView` _Stopwatch_ ⏱️
-- Create new phoenix "barebone" Phonenix application:
+[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/dwyl/phoenix-liveview-stopwatch/Elixir%20CI?label=build&style=flat-square)](https://github.com/dwyl/phoenix-liveview-stopwatch/actions/workflows/ci.yml)
+[![codecov.io](https://img.shields.io/codecov/c/github/dwyl/phoenix-liveview-stopwatch/main.svg?style=flat-square)](http://codecov.io/github/dwyl/phoenix-liveview-stopwatch?branch=main)
+[![Hex.pm](https://img.shields.io/hexpm/v/phoenix?color=brightgreen&style=flat-square)](https://hex.pm/packages/phoenix)
+[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat-square)](https://github.com/dwyl/phoenix-liveview-stopwatch/issues)
+[![HitCount](http://hits.dwyl.com/dwyl/phoenix-liveview-stopwatch.svg)](http://hits.dwyl.com/dwyl/phoenix-liveview-stopwatch)
+
+
+
+## Why? 💡
+
+We wanted to build the **simplest possible _shared_ stopwatch**
+as a self-contained
+[***experiment***](https://github.com/dwyl/technology-stack/issues/96)
+to test how easy complex/simple it would be
+before using this in our main
+[**`app`**](https://github.com/dwyl/app)
+
+## What? 🤷♀️
+
+`Phoenix LiveView` lets us build RealTime collaborative apps
+without writing a line of `JavaScript`.
+This is an _example_ that anyone can understand in **`10 mins`**.
+
+## How? 💻
+
+Try the finished app before you try to build it:
+
+https://liveview-stopwatch.fly.dev/
+
+![stopwatch](https://user-images.githubusercontent.com/194400/174432051-5199369d-df07-4809-a758-24d3738535f7.png)
+
+Once you've tried it, come back and **_build_ it**!
+### Create a new "barebones" Phonenix application:
```sh
mix phx.new stopwatch --no-mailer --no-dashboard --no-gettext --no-ecto
```
-- Create folders and files for liveView stopwatch code:
+### Create folders and files for `LiveView`:
```sh
mkdir lib/stopwatch_web/live
@@ -18,14 +50,21 @@ mkdir lib/stopwatch_web/templates/stopwatch
touch lib/stopwatch_web/templates/stopwatch/stopwatch.html.heex
```
-- Update router. In `lib/stopwatch_web/router.ex` update the "/" endpoint:
+### Update router
+
+In `lib/stopwatch_web/router.ex` update the "/" endpoint:
```elixir
live("/", StopwatchLive)
```
-- Create liveView logic (mount, render, handle_event, handle_info)
-in StopwatchLive module. In `lib/stopwatch_web/live/stopwatch_live.ex` add:
+### Create `LiveView` logic
+
+Create the
+`mount`, `render`, `handle_event` and `handle_info`
+functions
+in StopwatchLive module:
+`lib/stopwatch_web/live/stopwatch_live.ex`
```elixir
defmodule StopwatchWeb.StopwatchLive do
@@ -76,7 +115,11 @@ Finally the `handle_info` function manages the `:tick` event. If the status is
`:running` when send another `:tick` event after 1 second and increment the `:timer`
value with 1 second.
-- Update `lib/stopwatch_web/templates/layout/root.hml.heex` with the following body:
+### Update Root Template
+
+Update the
+`lib/stopwatch_web/templates/layout/root.hml.heex`
+with the following body:
```html
@@ -84,14 +127,19 @@ value with 1 second.
```
-- Create the `StopwatchView` module in `lib/stopwatch_web/views/stopwatch_view.ex`
+### Create View
+
+Create the `StopwatchView` module in `lib/stopwatch_web/views/stopwatch_view.ex`
```elixir defmodule StopwatchWeb.StopwatchView do
use StopwatchWeb, :view
end
```
-Finally create the templates in `lib/stopwatch_web/templates/stopwatch/stopwatch.html.heex`:
+### Create Template
+
+Finally create the templates in
+`lib/stopwatch_web/templates/stopwatch/stopwatch.html.heex`:
```html
<%= @time |> Time.truncate(:second) |> Time.to_string() %>
@@ -104,5 +152,257 @@ Finally create the templates in `lib/stopwatch_web/templates/stopwatch/stopwatch
<% end %>
```
-If you run the server with `mix phx.server` you should now be able
+If you run the server with
+`mix phx.server`
+you should now be able
to start/stop the stopwatch.
+
+## Sync Stopwatch
+
+So far the application will create a new timer for each client.
+That is good but doesn't really showcase the power of `LiveView`.
+We might aswell just be using _any_ other framework/library.
+To really see the power of using `LiveView`,
+we're going to use its' super power -
+lightweight websocket "channels" -
+to create a _collaborative_ stopwatch experience!
+
+
+
+To be able to sync a timer
+between all the connected clients
+we can move the stopwatch logic
+to its own module and use
+[`Agent`](https://elixir-lang.org/getting-started/mix-otp/agent.html).
+
+Create `lib/stopwatch/timer.ex` file and add the folowing content:
+
+```elixir
+defmodule Stopwatch.Timer do
+ use Agent
+ alias Phoenix.PubSub
+
+ def start_link(opts) do
+ Agent.start_link(fn -> {:stopped, ~T[00:00:00]} end, opts)
+ end
+
+ def get_timer_state(timer) do
+ Agent.get(timer, fn state -> state end)
+ end
+
+ def start_timer(timer) do
+ Agent.update(timer, fn {_timer_status, time} -> {:running, time} end)
+ notify()
+ end
+
+ def stop_timer(timer) do
+ Agent.update(timer, fn {_timer_status, time} -> {:stopped, time} end)
+ notify()
+ end
+
+ def tick(timer) do
+ Agent.update(timer, fn {timer_status, timer} ->
+ {timer_status, Time.add(timer, 1, :second)}
+ end)
+
+ notify()
+ end
+
+ def subscribe() do
+ PubSub.subscribe(Stopwatch.PubSub, "liveview_stopwatch")
+ end
+
+ def notify() do
+ PubSub.broadcast(Stopwatch.PubSub, "liveview_stopwatch", :timer_updated)
+ end
+end
+```
+
+The agent defines the state of the stopwatch
+as a tuple `{timer_status, time}`.
+We defined the
+`get_timer_state/1`, `start_timer/1`, `stop_timer/1`
+and `tick/1` functions
+which are responsible for updating the tuple.
+
+Finally the last two funtions:
+`subscribe/0` and `notify/0`
+are responsible for listening and sending
+the `:timer_updated` event via PubSub to the clients.
+
+
+Now we have the Timer agent defined
+we can tell the application to create
+a stopwatch when the application starts.
+Update the `lib/stopwatch/application.ex` file
+to add the `StopwatchTimer`
+in the supervision tree:
+
+```elixir
+ children = [
+ # Start the Telemetry supervisor
+ StopwatchWeb.Telemetry,
+ # Start the PubSub system
+ {Phoenix.PubSub, name: Stopwatch.PubSub},
+ # Start the Endpoint (http/https)
+ StopwatchWeb.Endpoint,
+ # Start a worker by calling: Stopwatch.Worker.start_link(arg)
+ # {Stopwatch.Worker, arg}
+ {Stopwatch.Timer, name: Stopwatch.Timer} # Create timer
+ ]
+```
+
+We define the timer name as `Stopwatch.Timer`.
+This name could be any `atom`
+and doesn't have to be an existing module name.
+It is just a unique way to find the timer.
+
+We can now update our `LiveView` logic
+to use the function defined in `Stopwatch.Timer`.
+Update
+`lib/stopwatch_web/live/stopwatch_live.ex`:
+
+```elixir
+defmodule StopwatchWeb.StopwatchLive do
+ use StopwatchWeb, :live_view
+
+ def mount(_params, _session, socket) do
+ if connected?(socket), do: Stopwatch.Timer.subscribe()
+
+ {timer_status, time} = Stopwatch.Timer.get_timer_state(Stopwatch.Timer)
+ {:ok, assign(socket, time: time, timer_status: timer_status)}
+ end
+
+ def render(assigns) do
+ Phoenix.View.render(StopwatchWeb.StopwatchView, "stopwatch.html", assigns)
+ end
+
+ def handle_event("start", _value, socket) do
+ Process.send_after(self(), :tick, 1000)
+ Stopwatch.Timer.start_timer(Stopwatch.Timer)
+ {:noreply, socket}
+ end
+
+ def handle_event("stop", _value, socket) do
+ Stopwatch.Timer.stop_timer(Stopwatch.Timer)
+ {:noreply, socket}
+ end
+
+ def handle_info(:timer_updated, socket) do
+ {timer_status, time} = Stopwatch.Timer.get_timer_state(Stopwatch.Timer)
+ {:noreply, assign(socket, time: time, timer_status: timer_status)}
+ end
+
+ def handle_info(:tick, socket) do
+ {timer_status, _time} = Stopwatch.Timer.get_timer_state(Stopwatch.Timer)
+
+ if timer_status == :running do
+ Process.send_after(self(), :tick, 1000)
+ Stopwatch.Timer.tick(Stopwatch.Timer)
+ {:noreply, socket}
+ else
+ {:noreply, socket}
+ end
+ end
+end
+```
+
+In `mount/3`, when the socket is connected
+we subscribe the client to the PubSub channel.
+This will allow our `LiveView`
+to listen for events from other clients.
+
+The `start`, `stop` and `tick` events
+are now calling the
+`start_timer`, `stop_timer` and `tick` functions
+from `Timer`,
+and we return `{:ok, socket}`
+without any changes on the `assigns`.
+All the updates are now done
+in the new
+`handle_info(:timer_updated, socket)`
+function.
+The `:timer_updated` event
+is sent by `PubSub`
+each time the timer state is changed.
+
+
+If you run the application:
+```sh
+mix phx.server
+```
+
+And open it in two different clients
+you should now have a synchronised stopwatch!
+
+![liveview-stopwatch-sync](https://user-images.githubusercontent.com/194400/174431168-d37e5382-f3e1-4c99-bd3b-bd3500a5035e.gif)
+
+To _test_ our new `Stopwatch.Timer` agent,
+we can add the following code to
+`test/stopwatch/timer_test.exs`:
+
+```elixir
+defmodule Stopwatch.TimerTest do
+ use ExUnit.Case, async: true
+
+ setup context do
+ start_supervised!({Stopwatch.Timer, name: context.test})
+ %{timer: context.test}
+ end
+
+ test "Timer agent is working!", %{timer: timer} do
+ assert {:stopped, ~T[00:00:00]} == Stopwatch.Timer.get_timer_state(timer)
+ assert :ok = Stopwatch.Timer.start_timer(timer)
+ assert :ok = Stopwatch.Timer.tick(timer)
+ assert {:running, time} = Stopwatch.Timer.get_timer_state(timer)
+ assert Time.truncate(time, :second) == ~T[00:00:01]
+ assert :ok = Stopwatch.Timer.stop_timer(timer)
+ assert {:stopped, _time} = Stopwatch.Timer.get_timer_state(timer)
+ end
+
+
+ test "Timer is reset", %{timer: timer} do
+ assert :ok = Stopwatch.Timer.start_timer(timer)
+ :ok = Stopwatch.Timer.tick(timer)
+ :ok = Stopwatch.Timer.tick(timer)
+ {:running, time} = Stopwatch.Timer.get_timer_state(timer)
+ assert Time.truncate(time, :second) == ~T[00:00:02]
+ Stopwatch.Timer.reset(timer)
+ assert {:stopped, ~T[00:00:00]} == Stopwatch.Timer.get_timer_state(timer)
+ end
+end
+```
+
+We use the `setup` function
+to create a new timer for each test.
+`start_supervised!` takes care of creating
+and stopping the process timer for the tests.
+Since `mix run` will automatically run the `Timer`
+defined in `application.ex`,
+i.e. the Timer with the name `Stopwatch.Timer`
+we want to create new timers
+for the tests using other names to avoid conflicts.
+This is why we use `context.test`
+to define the name of the test `Timer` process.
+
+
+## What's next?
+
+If you found this example useful,
+please ⭐️ the GitHub repository
+so we (_and others_) know you liked it!
+
+Your feedback is always very welcome!
+
+If you think of other features
+you want to add,
+please
+[**open an issue**](https://github.com/dwyl/phoenix-liveview-stopwatch/issues)
+to discuss!
diff --git a/coveralls.json b/coveralls.json
new file mode 100644
index 0000000..f6c5575
--- /dev/null
+++ b/coveralls.json
@@ -0,0 +1,14 @@
+{
+ "coverage_options": {
+ "minimum_coverage": 100
+ },
+ "skip_files": [
+ "test/",
+ "lib/stopwatch/application.ex",
+ "lib/stopwatch/release.ex",
+ "lib/stopwatch_web.ex",
+ "lib/stopwatch_web/views/error_helpers.ex",
+ "lib/stopwatch_web/channels/user_socket.ex",
+ "lib/stopwatch_web/telemetry.ex"
+ ]
+}
\ No newline at end of file
diff --git a/lib/stopwatch/application.ex b/lib/stopwatch/application.ex
index 7253018..cb3ef4a 100644
--- a/lib/stopwatch/application.ex
+++ b/lib/stopwatch/application.ex
@@ -13,9 +13,10 @@ defmodule Stopwatch.Application do
# Start the PubSub system
{Phoenix.PubSub, name: Stopwatch.PubSub},
# Start the Endpoint (http/https)
- StopwatchWeb.Endpoint
+ StopwatchWeb.Endpoint,
# Start a worker by calling: Stopwatch.Worker.start_link(arg)
# {Stopwatch.Worker, arg}
+ {Stopwatch.Timer, name: Stopwatch.Timer}
]
# See https://hexdocs.pm/elixir/Supervisor.html
diff --git a/lib/stopwatch/timer.ex b/lib/stopwatch/timer.ex
new file mode 100644
index 0000000..fef15ad
--- /dev/null
+++ b/lib/stopwatch/timer.ex
@@ -0,0 +1,43 @@
+defmodule Stopwatch.Timer do
+ use Agent
+ alias Phoenix.PubSub
+
+ def start_link(opts) do
+ Agent.start_link(fn -> {:stopped, ~T[00:00:00]} end, opts)
+ end
+
+ def get_timer_state(timer) do
+ Agent.get(timer, fn state -> state end)
+ end
+
+ def start_timer(timer) do
+ Agent.update(timer, fn {_timer_status, time} -> {:running, time} end)
+ notify()
+ end
+
+ def stop_timer(timer) do
+ Agent.update(timer, fn {_timer_status, time} -> {:stopped, time} end)
+ notify()
+ end
+
+ def tick(timer) do
+ Agent.update(timer, fn {timer_status, timer} ->
+ {timer_status, Time.add(timer, 1, :second)}
+ end)
+
+ notify()
+ end
+
+ def reset(timer) do
+ Agent.update(timer, fn _state -> {:stopped, ~T[00:00:00]} end)
+ notify()
+ end
+
+ def subscribe() do
+ PubSub.subscribe(Stopwatch.PubSub, "liveview_stopwatch")
+ end
+
+ def notify() do
+ PubSub.broadcast(Stopwatch.PubSub, "liveview_stopwatch", :timer_updated)
+ end
+end
diff --git a/lib/stopwatch_web/controllers/page_controller.ex b/lib/stopwatch_web/controllers/page_controller.ex
deleted file mode 100644
index 1638e9f..0000000
--- a/lib/stopwatch_web/controllers/page_controller.ex
+++ /dev/null
@@ -1,7 +0,0 @@
-defmodule StopwatchWeb.PageController do
- use StopwatchWeb, :controller
-
- def index(conn, _params) do
- render(conn, "index.html")
- end
-end
diff --git a/lib/stopwatch_web/live/stopwatch_live.ex b/lib/stopwatch_web/live/stopwatch_live.ex
index e87d440..b4f638a 100644
--- a/lib/stopwatch_web/live/stopwatch_live.ex
+++ b/lib/stopwatch_web/live/stopwatch_live.ex
@@ -2,7 +2,10 @@ defmodule StopwatchWeb.StopwatchLive do
use StopwatchWeb, :live_view
def mount(_params, _session, socket) do
- {:ok, assign(socket, time: ~T[00:00:00], timer_status: :stopped)}
+ if connected?(socket), do: Stopwatch.Timer.subscribe()
+
+ {timer_status, time} = Stopwatch.Timer.get_timer_state(Stopwatch.Timer)
+ {:ok, assign(socket, time: time, timer_status: timer_status)}
end
def render(assigns) do
@@ -11,18 +14,32 @@ defmodule StopwatchWeb.StopwatchLive do
def handle_event("start", _value, socket) do
Process.send_after(self(), :tick, 1000)
- {:noreply, assign(socket, :timer_status, :running)}
+ Stopwatch.Timer.start_timer(Stopwatch.Timer)
+ {:noreply, socket}
end
def handle_event("stop", _value, socket) do
- {:noreply, assign(socket, :timer_status, :stopped)}
+ Stopwatch.Timer.stop_timer(Stopwatch.Timer)
+ {:noreply, socket}
+ end
+
+ def handle_event("reset", _value, socket) do
+ Stopwatch.Timer.reset(Stopwatch.Timer)
+ {:noreply, socket}
+ end
+
+ def handle_info(:timer_updated, socket) do
+ {timer_status, time} = Stopwatch.Timer.get_timer_state(Stopwatch.Timer)
+ {:noreply, assign(socket, time: time, timer_status: timer_status)}
end
def handle_info(:tick, socket) do
- if socket.assigns.timer_status == :running do
+ {timer_status, _time} = Stopwatch.Timer.get_timer_state(Stopwatch.Timer)
+
+ if timer_status == :running do
Process.send_after(self(), :tick, 1000)
- time = Time.add(socket.assigns.time, 1, :second)
- {:noreply, assign(socket, :time, time)}
+ Stopwatch.Timer.tick(Stopwatch.Timer)
+ {:noreply, socket}
else
{:noreply, socket}
end
diff --git a/lib/stopwatch_web/router.ex b/lib/stopwatch_web/router.ex
index 14a7681..68dc067 100644
--- a/lib/stopwatch_web/router.ex
+++ b/lib/stopwatch_web/router.ex
@@ -10,18 +10,9 @@ defmodule StopwatchWeb.Router do
plug(:put_secure_browser_headers)
end
- pipeline :api do
- plug(:accepts, ["json"])
- end
-
scope "/", StopwatchWeb do
pipe_through(:browser)
live("/", StopwatchLive)
end
-
- # Other scopes may use custom stacks.
- # scope "/api", StopwatchWeb do
- # pipe_through :api
- # end
end
diff --git a/lib/stopwatch_web/templates/stopwatch/stopwatch.html.heex b/lib/stopwatch_web/templates/stopwatch/stopwatch.html.heex
index 843a74a..aa74b03 100644
--- a/lib/stopwatch_web/templates/stopwatch/stopwatch.html.heex
+++ b/lib/stopwatch_web/templates/stopwatch/stopwatch.html.heex
@@ -7,3 +7,5 @@
Stop
<% end %>
+Reset
+
diff --git a/mix.exs b/mix.exs
index a003879..1005d95 100644
--- a/mix.exs
+++ b/mix.exs
@@ -10,7 +10,16 @@ defmodule Stopwatch.MixProject do
compilers: Mix.compilers(),
start_permanent: Mix.env() == :prod,
aliases: aliases(),
- deps: deps()
+ deps: deps(),
+ test_coverage: [tool: ExCoveralls],
+ preferred_cli_env: [
+ c: :test,
+ coveralls: :test,
+ "coveralls.detail": :test,
+ "coveralls.post": :test,
+ "coveralls.html": :test,
+ "coveralls.json": :test
+ ],
]
end
@@ -42,7 +51,10 @@ defmodule Stopwatch.MixProject do
{:telemetry_metrics, "~> 0.6"},
{:telemetry_poller, "~> 1.0"},
{:jason, "~> 1.2"},
- {:plug_cowboy, "~> 2.5"}
+ {:plug_cowboy, "~> 2.5"},
+
+ # Check test coverage: hex.pm/packages/excoveralls
+ {:excoveralls, "~> 0.14.5", only: :test},
]
end
@@ -54,6 +66,7 @@ defmodule Stopwatch.MixProject do
# See the documentation for `Mix` for more info on aliases.
defp aliases do
[
+ c: ["coveralls.html"],
setup: ["deps.get"],
"assets.deploy": ["esbuild default --minify", "phx.digest"]
]
diff --git a/mix.lock b/mix.lock
index 4adfb0e..0c3c15d 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,14 +1,21 @@
%{
"castore": {:hex, :castore, "0.1.17", "ba672681de4e51ed8ec1f74ed624d104c0db72742ea1a5e74edbc770c815182f", [:mix], [], "hexpm", "d9844227ed52d26e7519224525cb6868650c272d4a3d327ce3ca5570c12163f9"},
+ "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
"cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
"esbuild": {:hex, :esbuild, "0.5.0", "d5bb08ff049d7880ee3609ed5c4b864bd2f46445ea40b16b4acead724fb4c4a3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "f183a0b332d963c4cfaf585477695ea59eef9a6f2204fdd0efa00e099694ffe5"},
+ "excoveralls": {:hex, :excoveralls, "0.14.5", "5c685449596e962c779adc8f4fb0b4de3a5b291c6121097572a3aa5400c386d3", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9b4a9bf10e9a6e48b94159e13b4b8a1b05400f17ac16cc363ed8734f26e1f4e"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"floki": {:hex, :floki, "0.32.1", "dfe3b8db3b793939c264e6f785bca01753d17318d144bd44b407fb3493acaa87", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "d4b91c713e4a784a3f7b1e3cc016eefc619f6b1c3898464222867cafd3c681a3"},
+ "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
+ "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"},
+ "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"},
+ "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
+ "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"phoenix": {:hex, :phoenix, "1.6.10", "7a9e8348c5c62e7fd2f74a1884b88d98251f87186a430048bfbdbab3e3f46736", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "08cf70d42f61dd0ea381805bac3cddef57b7b92ade5acc6f6036aa25ecaca9a2"},
"phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"},
@@ -19,7 +26,9 @@
"plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"},
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
+ "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
+ "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
}
diff --git a/test/stopwatch/timer_test.exs b/test/stopwatch/timer_test.exs
new file mode 100644
index 0000000..8b13322
--- /dev/null
+++ b/test/stopwatch/timer_test.exs
@@ -0,0 +1,28 @@
+defmodule Stopwatch.TimerTest do
+ use ExUnit.Case, async: true
+
+ setup context do
+ start_supervised!({Stopwatch.Timer, name: context.test})
+ %{timer: context.test}
+ end
+
+ test "Timer agent is working!", %{timer: timer} do
+ assert {:stopped, ~T[00:00:00]} == Stopwatch.Timer.get_timer_state(timer)
+ assert :ok = Stopwatch.Timer.start_timer(timer)
+ assert :ok = Stopwatch.Timer.tick(timer)
+ assert {:running, time} = Stopwatch.Timer.get_timer_state(timer)
+ assert Time.truncate(time, :second) == ~T[00:00:01]
+ assert :ok = Stopwatch.Timer.stop_timer(timer)
+ assert {:stopped, _time} = Stopwatch.Timer.get_timer_state(timer)
+ end
+
+ test "Timer is reset", %{timer: timer} do
+ assert :ok = Stopwatch.Timer.start_timer(timer)
+ :ok = Stopwatch.Timer.tick(timer)
+ :ok = Stopwatch.Timer.tick(timer)
+ {:running, time} = Stopwatch.Timer.get_timer_state(timer)
+ assert Time.truncate(time, :second) == ~T[00:00:02]
+ Stopwatch.Timer.reset(timer)
+ assert {:stopped, ~T[00:00:00]} == Stopwatch.Timer.get_timer_state(timer)
+ end
+end
diff --git a/test/stopwatch_web/live/stopwatch_live_test.exs b/test/stopwatch_web/live/stopwatch_live_test.exs
index a03285e..6fc778c 100644
--- a/test/stopwatch_web/live/stopwatch_live_test.exs
+++ b/test/stopwatch_web/live/stopwatch_live_test.exs
@@ -2,17 +2,16 @@ defmodule StopwatchW.StopwatchLiveTest do
use StopwatchWeb.ConnCase
import Phoenix.LiveViewTest
- test "disconnected and connected mount", %{conn: conn} do
+ test "stopwatch is ticking", %{conn: conn} do
conn = get(conn, "/")
assert html_response(conn, 200) =~ "00:00:00 "
- {:ok, _view, _html} = live(conn)
- end
-
- test "test timer is running", %{conn: conn} do
- {:ok, view, _html} = live(conn, "/")
+ {:ok, view, _html} = live(conn)
render_click(view, "start")
Process.sleep(1000)
assert render_click(view, "stop") =~ "00:00:01"
+
+ # reset
+ assert render_click(view, "reset") =~ "Start"
end
end