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 @@ <% end %> + + 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