{"id":13507890,"url":"https://github.com/dwyl/phoenix-chat-example","last_synced_at":"2025-03-30T09:33:09.081Z","repository":{"id":28951087,"uuid":"117125157","full_name":"dwyl/phoenix-chat-example","owner":"dwyl","description":"💬 The Step-by-Step Beginners Tutorial for Building, Testing \u0026 Deploying a Chat app in Phoenix 1.7 [Latest] 🚀","archived":false,"fork":false,"pushed_at":"2025-03-10T18:33:37.000Z","size":920,"stargazers_count":794,"open_issues_count":1,"forks_count":97,"subscribers_count":111,"default_branch":"main","last_synced_at":"2025-03-10T19:24:56.216Z","etag":null,"topics":["beginner","chat","deployment","ecto","elixir","elixir-lang","heroku","learn","phoenix","phoenix-chat","phoenix-framework","realtime","step-by-step","testing","tutorial"],"latest_commit_sha":null,"homepage":"https://phoenix-chat.fly.dev","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dwyl.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-01-11T16:36:27.000Z","updated_at":"2025-03-10T18:33:33.000Z","dependencies_parsed_at":"2023-02-18T08:31:21.070Z","dependency_job_id":"5434859c-3279-4c78-a550-663bf29ad67f","html_url":"https://github.com/dwyl/phoenix-chat-example","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwyl%2Fphoenix-chat-example","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwyl%2Fphoenix-chat-example/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwyl%2Fphoenix-chat-example/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwyl%2Fphoenix-chat-example/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dwyl","download_url":"https://codeload.github.com/dwyl/phoenix-chat-example/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246301963,"owners_count":20755512,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["beginner","chat","deployment","ecto","elixir","elixir-lang","heroku","learn","phoenix","phoenix-chat","phoenix-framework","realtime","step-by-step","testing","tutorial"],"created_at":"2024-08-01T02:00:42.122Z","updated_at":"2025-03-30T09:33:08.788Z","avatar_url":"https://github.com/dwyl.png","language":"Elixir","readme":"\u003cdiv align=\"center\"\u003e\n\n# Phoenix Chat Example\n\n![phoenix-chat-logo](https://user-images.githubusercontent.com/194400/39481553-c448aa1c-4d63-11e8-9389-47789833a96e.png)\n\n![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/dwyl/phoenix-chat-example/ci.yml?label=build\u0026style=flat-square\u0026branch=main)\n[![codecov.io](https://img.shields.io/codecov/c/github/dwyl/phoenix-chat-example/main.svg?style=flat-square)](https://codecov.io/github/dwyl/phoenix-chat-example?branch=main)\n[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat-square)](https://github.com/dwyl/phoenix-chat-example/issues)\n[![HitCount](https://hits.dwyl.com/dwyl/phoenix-chat-example.svg)](https://github.com/dwyl/phoenix-chat-example)\n[![Hex pm](https://img.shields.io/hexpm/v/phoenix.svg?style=flat-square)](https://hex.pm/packages/phoenix)\n  \n_Try_ it: \n[**phoenix-chat**.fly.dev](https://phoenix-chat.fly.dev/)\n\u003c!-- [![Deps Status](https://beta.hexfaktor.org/badge/all/github/dwyl/phoenix-chat-example.svg?style=flat-square)](https://beta.hexfaktor.org/github/dwyl/phoenix-chat-example) --\u003e\n\u003c!-- [![Inline docs](https://inch-ci.org/github/dwyl/phoenix-chat-example.svg?style=flat-square)](https://inch-ci.org/github/dwyl/phoenix-chat-example) --\u003e\n\n\nA ***step-by-step tutorial*** for building, testing\nand _deploying_ a Chat app in Phoenix!\n\n\u003c/div\u003e\n\n- [Phoenix Chat Example](#phoenix-chat-example)\n  - [Why?](#why)\n  - [What?](#what)\n  - [Who?](#who)\n- [_How_?](#how)\n  - [0. Pre-requisites (_Before you Start_)](#0-pre-requisites-before-you-start)\n    - [_Check_ You Have Everything _Before_ Starting](#check-you-have-everything-before-starting)\n  - [First _Run_ the _Finished_ App](#first-run-the-finished-app)\n    - [Clone the Project:](#clone-the-project)\n    - [Install the Dependencies](#install-the-dependencies)\n    - [Run the App](#run-the-app)\n  - [1. _Create_ The _App_](#1-create-the-app)\n    - [Run the Tests](#run-the-tests)\n  - [2. _Create_ the (WebSocket) \"_Channel_\"](#2-create-the-websocket-channel)\n  - [3. Update the Template File (UI)](#3-update-the-template-file-ui)\n    - [3.1 Update Layout Template](#31-update-layout-template)\n    - [3.2 Update the `page_controller_test.exs`](#32-update-the-page_controller_testexs)\n  - [4. Update the \"Client\" code in App.js](#4-update-the-client-code-in-appjs)\n    - [4.1 Comment Out Lines in `user_socket.js`](#41-comment-out-lines-in-user_socketjs)\n    - [Storing Chat Message Data/History](#storing-chat-message-datahistory)\n  - [5. Generate Database Schema to Store Chat History](#5-generate-database-schema-to-store-chat-history)\n  - [6. Run the Ecto Migration (_Create The Database Table_)](#6-run-the-ecto-migration-create-the-database-table)\n    - [6.1 Review the Messages Table Schema](#61-review-the-messages-table-schema)\n  - [7. Insert Messages into Database](#7-insert-messages-into-database)\n  - [8. Load _Existing_ Messages (_When Someone Joins the Chat_)](#8-load-existing-messages-when-someone-joins-the-chat)\n  - [9. Send Existing Messages to the Client when they Join](#9-send-existing-messages-to-the-client-when-they-join)\n  - [10. _Checkpoint_: Our Chat App Saves Messages!! (_Try it_!)](#10-checkpoint-our-chat-app-saves-messages-try-it)\n- [Testing our App (_Automated Testing_)](#testing-our-app-automated-testing)\n  - [11. Run the Default/Generated Tests](#11-run-the-defaultgenerated-tests)\n  - [12. Understanding The Channel Tests](#12-understanding-the-channel-tests)\n    - [12.1 _Analyse_ a Test](#121-analyse-a-test)\n  - [13. What is _Not_ Tested?](#13-what-is-not-tested)\n    - [13.1 Add `excoveralls` as a (Development) Dependency to `mix.exs`](#131-add-excoveralls-as-a-development-dependency-to-mixexs)\n    - [13.2 Create a _New File_ Called `coveralls.json`](#132-create-a-new-file-called-coverallsjson)\n    - [13.3 Run the Tests with Coverage Checking](#133-run-the-tests-with-coverage-checking)\n    - [13.4 Write a Test for the Untested Function](#134-write-a-test-for-the-untested-function)\n- [Authentication](#authentication)\n- [Adding `Presence` to track who's online](#adding-presence-to-track-whos-online)\n- [Continuous Integration](#continuous-integration)\n- [Deployment!](#deployment)\n  - [What _Next_?](#what-next)\n  - [Inspiration](#inspiration)\n  - [Recommended Reading / Learning](#recommended-reading--learning)\n\n## Why?\n\nChat apps are the \n[`\"Hello World\"`](https://en.wikipedia.org/wiki/%22Hello,_World!%22_program) \nof \n[real time](https://en.wikipedia.org/wiki/Real-time_computing)\nexamples. \u003cbr /\u003e\n\nSadly, **_most_ example apps** show a few **basics**\nand then **ignore the rest** ... 🤷‍♀️\u003cbr /\u003e\nSo **beginners** are often left **lost** or **confused** as to\nwhat they should _do_ or learn _next_! \u003cbr /\u003e\nVery _few_ tutorials consider \n**Testing, Deployment, Documentation** or _other_ \"**Enhancements**\" \nwhich are all part of the \"***Real World***\" \nof building and running apps;\nso those are topics we **_will_ cover** to \"_fill in the gaps_\".\n\nWe wrote _this_ tutorial to be **_easiest_ way to learn `Phoenix`**,\n`Ecto` and `Channels` with a **_practical_ example _anyone_ can follow**.\n\nThis is the example/tutorial we _wished_ we had \nwhen we were learning `Elixir`, `Phoenix` ...\nIf you find it useful, please ⭐ 🙏 Thanks!\n\n\n## What?\n\nA simple step-by-step tutorial showing you how to:\n\n+ **Create** a **Phoenix App** from _scratch_\n(_using the `mix phx.new chat` \"generator\" command_)\n+ Add a \"Channel\" so your app can communicate over \n  [**WebSockets**](https://en.wikipedia.org/wiki/WebSocket).\n+ Implement a _basic_ ***front-end*** in _plain_ JavaScript\n(_ES5 without any libraries_) to interact with Phoenix\n(_send/receive messages via WebSockets_)\n+ Add a simple \"**Ecto**\" **schema** to define\nthe **Database Table** (_to store messages_)\n+ **Write** the functions (\"CRUD\") to _save_\nmessage/sender data to a database table.\n+ **Test** that everything is working as expected.\n+ ***Deploy*** to **`Fly.io`** so you can _show_ people your creation!\n\n_Initially_, we _deliberately_ skip over configuration files\nand \"_Phoenix Internals_\"\nbecause you (_beginners_) _don't need_ to know about them to get _started_.\nBut don't worry, we will return to them when _needed_.\nWe favour \"_just-in-time_\" (_when you need it_) learning\nas it's _immediately_ obvious and _practical_ ***why***\nwe are learning something.\n\n\n## Who?\n\nThis example is for ***complete beginners***\nas a \"***My First Phoenix***\" App. \u003cbr /\u003e\n\nWe try to _assume_ as little as possible,\nbut if you think we \"_skipped a step_\"\nor  you feel \"_stuck_\" for any reason,\nor have _any_ questions (_related to this example_),\nplease open an issue on GitHub! \u003cbr /\u003e\nBoth the @dwyl and Phoenix communities are _super **beginner-friendly**_,\nso don't be afraid/shy. \u003cbr /\u003e\nAlso, by asking questions, you are helping everyone\nthat is or might be stuck with the _same_ thing!\n+ **Chat App _specific_** questions:\n[dwyl/**phoenix-chat-example**/issues](https://github.com/dwyl/phoenix-chat-example/issues)\n+ **General** Learning Phoenix questions:\n[dwyl/learn-**phoenix-framework**/issues](https://github.com/dwyl/learn-phoenix-framework/issues)\n\n\n# _How_?\n\nThese instructions show you how to _create_ the Chat app\n_from scratch_.\n\u003c!--\nIf you prefer to _run_ the existing/sample app,\nscroll down to the \"Clone Repo and Run on Localhost\" section instead.\n--\u003e\n\n## 0. Pre-requisites (_Before you Start_)\n\n1. **Elixir _Installed_** on your **local machine**. \u003cbr /\u003e\nsee: \n[dwyl/learn-elixir#**installation**](https://github.com/dwyl/learn-elixir#installation) \u003cbr /\u003e\ne.g:\n\n```sh\nbrew install elixir\n```\n\u003e _**Note**: if you already have `Elixir` installed on your Mac,\n  and just want to upgrade to the latest version, run:_\n  **`brew upgrade elixir`**\n\n\n1. **Phoenix** framework **installed**.\nsee: \n[hexdocs.pm/phoenix/installation.html](https://hexdocs.pm/phoenix/installation.html) \u003cbr /\u003e\ne.g:\n\n```sh\nmix archive.install hex phx_new\n```\n\n1. PostgreSQL (Database Server) installed (_to save chat messages_) \u003cbr /\u003e\nsee: \n[dwyl/**learn-postgresql#installation**](https://github.com/dwyl/learn-postgresql#installation)\n\n\u003c!-- update instructions to https://hexdocs.pm/phoenix/installation.html --\u003e\n\n1. Basic **Elixir Syntax** knowledge will help,\u003cbr /\u003e\nplease see:\n[dwyl/**learn-elixir**](https://github.com/dwyl/learn-elixir)\n\n1. Basic **JavaScript** knowledge is _advantageous_\n(_but not essential as the \"front-end\" code\nis quite basic and well-commented_).\nsee: \n[dwyl/Javascript-the-Good-Parts-notes](https://github.com/dwyl/Javascript-the-Good-Parts-notes)\n\n\n### _Check_ You Have Everything _Before_ Starting\n\nCheck you have the _latest version_ of **Elixir**\n(_run the following command in your terminal_):\n\n```sh\nelixir -v\n```\n\nYou should see something like:\n\n```sh\nErlang/OTP 25 [erts-13.1.1] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace]\n\nElixir 1.14.1 (compiled with Erlang/OTP 25)\n```\n\nCheck you have the **latest** version of **Phoenix**:\n\n```sh\nmix phx.new -v\n```\n\nYou should see:\n\n```sh\nPhoenix installer v1.7.0-rc.2\n```\n\n\u003e **Note**: if your `Phoenix` version is _newer_,\n\u003e Please feel free to update this doc! 📝\n\u003e We try our best to keep it updated ...\n\u003e but _your_ contributions are always welcome!\n\n\u003e In this tutorial, \n\u003e we are using \n\u003e [Phoenix 1.7-rc2](https://github.com/phoenixframework/phoenix/blob/master/CHANGELOG.md#170-rc2-2023-01-13),\n\u003e the second release candidate \n\u003e for `Phoenix 1.7`.\n\u003e At the time of writing, \n\u003e if you install Phoenix,\n\u003e the *latest stable version* is not `v1.7`.\n\u003e To use this version,\n\u003e follow the official guide (don't worry, it's just running one command!)\n\u003e -\u003e https://www.phoenixframework.org/blog/phoenix-1.7-released\n\u003e \n\u003e However, if you are reading this after its release,\n\u003e `v1.7` will be installed for you, \n\u003e and you should see\n\u003e `Phoenix installer v1.7.0`\n\u003e in your terminal.\n\n\n_Confirm_ **PostgreSQL** is running (_so the App can store chat messages_)\nrun the following command:\n\n```sh\nlsof -i :5432\n```\n\nYou should see output _similar_ to the following:\n\n```sh\nCOMMAND  PID  USER   FD  TYPE DEVICE                  SIZE/OFF NODE NAME\npostgres 529 Nelson  5u  IPv6 0xbc5d729e529f062b      0t0  TCP localhost:postgresql (LISTEN)\npostgres 529 Nelson  6u  IPv4 0xbc5d729e55a89a13      0t0  TCP localhost:postgresql (LISTEN)\n```\n\nThis tells us that PostgreSQL is \"_listening_\" on TCP Port `5432`\n(_the default port_)\n\nIf the `lsof` command does not yield any result\nin your terminal,\nrun:\n\n```sh\npg_isready\n```\n\nIt should print the following:\n\n```sh\n/tmp:5432 - accepting connections\n```\n\nWith all those \n[\"pre-flight checks\"](https://en.wikipedia.org/wiki/Preflight_checklist) \nperformed, let's _fly_! 🚀\n\n\u003cbr /\u003e\n\n## First _Run_ the _Finished_ App\n\n_Before_ you attempt to build the Chat App from scratch,\nclone and run the _finished_ working version\nto get an idea of what to expect.\n\n### Clone the Project:\n\nIn your terminal run the following command to clone the repo:\n\n```sh\ngit clone git@github.com:dwyl/phoenix-chat-example.git\n```\n\n### Install the Dependencies\n\nChange into the `phoenix-chat-example` directory\nand install both the `Elixir` and `Node.js` dependencies\nwith this command:\n\n```sh\ncd phoenix-chat-example\nmix setup\n```\n\n\u003c!-- ### TODO: Add auth step? --\u003e\n\n\n### Run the App\n\nRun the Phoenix app with the command:\n\n```sh\nmix phx.server\n```\n\nIf you open the app\n[localhost:4000](http://localhost:4000)\nin two more web browsers,\nyou can see the chat messages\ndisplayed in all of them\nas soon as you hit the \u003ckbd\u003eEnter\u003c/kbd\u003e key:\n\n![phoenix-chat-example-tailwind-ui-with-auth](https://user-images.githubusercontent.com/194400/204945771-fa4f4c2a-b055-4ef2-93f0-fe0c6b8f4466.gif)\n\n\u003cbr /\u003e\n\nNow that you have confirmed that the _finished_\nphoenix chat app works on your machine,\nit's time to _build_ it from scratch!\n\nChange directory:\n\n```sh\ncd ..\n```\n\nAnd start building!\n\n\n\u003cbr /\u003e\n\n## 1. _Create_ The _App_\n\nIn your terminal program on your localhost,\ntype the following command to create the app:\n\n```sh\nmix phx.new chat --no-mailer --no-dashboard --no-gettext\n```\nThat will create the directory structure and project files. \u003cbr /\u003e\n\n\u003e We are running the \n\u003e [`mix phx.new` command](https://hexdocs.pm/phoenix/Mix.Tasks.Phx.New.html)\n\u003e with the `--no-mailer` `--no-dashboard` `--no-gettext` arguments\n\u003e because we don't want our project\n\u003e to generate mailer files, \n\u003e to include a `Phoenix.LiveDashboard` \n\u003e and generate `gettext` files \n\u003e (for [`i18n`](https://en.wikipedia.org/wiki/Internationalization_and_localization)).\n\nWhen asked to \"***Fetch and install dependencies***? [Yn]\",\u003cbr /\u003e\nType \u003ckbd\u003eY\u003c/kbd\u003e in your terminal,\nfollowed by the \u003ckbd\u003eEnter\u003c/kbd\u003e (\u003ckbd\u003eReturn\u003c/kbd\u003e) key.\n\nYou should see: \u003cbr /\u003e\n![fetch-and-install-dependencies](https://user-images.githubusercontent.com/194400/34833220-d219221c-f6e6-11e7-88d6-87aa4c3054e4.png)\n\nChange directory into the `chat` directory by running the suggested command:\n```sh\ncd chat\n```\n\nNow run the following command:\n\n```sh\nmix setup\n```\n\n\u003e _**Note**: at this point there is already an \"App\"\nit just does not **do** anything (yet) ... \u003cbr /\u003e\nyou **can** run `mix phx.server`\nin your terminal - don't worry if you're seeing error \u003cbr /\u003e\nmessages, this is because we haven't created our database yet. \u003cbr /\u003e\nWe will take care of that in [step 6](#6-createconfigure-database)!\u003cbr /\u003e\nFor now, open [http://localhost:4000](http://localhost:4000)\nin your browser \u003cbr /\u003e\nand you will see the `default`\n\"Welcome to Phoenix\" homepage:_ \u003cbr /\u003e\n\n![welcome-to-phoenix](https://user-images.githubusercontent.com/17494745/216576178-a227a6ef-ad12-4b74-9b29-4913b5e298bc.png)\n\nShut down the Phoenix server in your terminal\nwith the\n\u003ckbd\u003ectrl\u003c/kbd\u003e+\u003ckbd\u003eC\u003c/kbd\u003e\ncommand.\n\n### Run the Tests\n\nIn your terminal window, run the following command:\n\n```\nmix test\n```\n\nYou should see output similar to the following:\n\n```sh\nGenerated chat app\n.....\nFinished in 0.02 seconds (0.02s async, 0.00s sync)\n5 tests, 0 failures\n\nRandomized with seed 84184\n```\n\nNow that we have confirmed that everything is working (all tests pass),\nlet's continue to the _interesting_ part!\n\n\u003cbr /\u003e\n\n## 2. _Create_ the (WebSocket) \"_Channel_\"\n\nGenerate the (WebSocket) channel to be used in the chat app:\n\n```sh\nmix phx.gen.channel Room\n```\n\n\u003e If you are prompted to confirm installation of a new socket handler\ntype `y` and hit the `[Enter]` key.\n\nThis will create **three files**:\u003cbr /\u003e\n\n```sh\n* creating lib/chat_web/channels/room_channel.ex\n* creating test/chat_web/channels/room_channel_test.exs\n* creating test/support/channel_case.ex\n```\n\nin addition to creating **two more files**:\n```sh\n* creating lib/chat_web/channels/user_socket.ex\n* creating assets/js/user_socket.js\n```\n\n\nThe `room_channel.ex` file handles receiving/sending messages\nand the `room_channel_test.exs` tests basic interaction with the channel.\nWe'll focus on the `socket` files created afterwards.\n(_Don't worry about this yet, we will look at the test file in step 14 below_!)\n\nWe are informed that we need to update a piece of code in our app:\n```sh\nAdd the socket handler to your `lib/chat_web/endpoint.ex`, for example:\n\n    socket \"/socket\", ChatWeb.UserSocket,\n      websocket: true,\n      longpoll: false\n\nFor the front-end integration, you need to import the `user_socket.js`\nin your `assets/js/app.js` file:\n\n    import \"./user_socket.js\"\n```\n\nThe generator asks us to import the client code in the frontend.\nLet's do that later. For now, open the `lib/chat_web/endpoint.ex` file and follow the instructions.\n\nAfter this, open the file called `/lib/chat_web/channels/user_socket.ex` \u003cbr \u003e\nand change the line:\n\n```elixir\nchannel \"room:*\", ChatWeb.RoomChannel\n```\n\nto:\n\n```elixir\nchannel \"room:lobby\", ChatWeb.RoomChannel\n```\n\nCheck the change [here](https://github.com/dwyl/phoenix-chat-example/blob/0faa7f18ea6d7790e027ace5147cd1740040a75e/lib/chat_web/channels/user_socket.ex#L11).\n\nThis will ensure that whatever messages that are sent to `\"room:lobby\"` are routed to our `RoomChannel`.\n\nThe previous `\"room.*` meant that any subtopic within `\"room\"` were routed. \nBut for now, let's narrow down to just one subtopic :smile:.\n\n\u003e For more detail on Phoenix Channels,\n(_we highly recommend you_) read:\nhttps://hexdocs.pm/phoenix/channels.html\n\n\n\u003cbr /\u003e\n\n## 3. Update the Template File (UI)\n\nOpen the the\n[`/lib/chat_web/controllers/page_html/home.html.heex`](/lib/chat_web/controllers/page_html/home.html.heex)\nfile \u003cbr /\u003e\nand _copy-paste_ (_or type_) the following code:\n\n```html\n\u003c!-- The list of messages will appear here: --\u003e\n\u003cdiv class=\"mt-[4rem]\"\u003e\n  \u003cul id=\"msg-list\" phx-update=\"append\" class=\"pa-1\"\u003e\u003c/ul\u003e\n\u003c/div\u003e\n\n\u003cfooter class=\"bg-slate-800 p-2 h-[3rem] fixed bottom-0 w-full flex justify-center\"\u003e\n  \u003cdiv class=\"w-full flex flex-row items-center text-gray-700 focus:outline-none font-normal\"\u003e\n    \u003cinput type=\"text\" id=\"name\" placeholder=\"Name\" required\n        class=\"grow-0 w-1/6 px-1.5 py-1.5\"/\u003e\n\n    \u003cinput type=\"text\" id=\"msg\" placeholder=\"Your message\" required\n      class=\"grow w-2/3 mx-1 px-2 py-1.5\"/\u003e\n\n    \u003cbutton id=\"send\" class=\"text-white bold rounded px-3 py-1.5 w-fit\n        transition-colors duration-150 bg-sky-500 hover:bg-sky-600\"\u003e\n      Send\n    \u003c/button\u003e\n  \u003c/div\u003e\n\u003c/footer\u003e\n\n```\n\nThis is the _basic_ form we will use to input Chat messages. \u003cbr /\u003e\nThe classes e.g. `w-full` and `items-center`\nare [`TailwindCSS`](https://tailwindcss.com/)\nclasses to _style_ the form. \u003cbr /\u003e\nPhoenix includes Tailwind by default so you can get up-and-running\nwith your App/Idea/\"MVP\"! \u003cbr /\u003e\n\n\u003e If you're new to `Tailwind`,\nplease see: \n[dwyl/**learn-tailwind**](https://github.com/dwyl/learn-tailwind)\n\u003e \n\u003e If you have questions about any \nof the **`Tailwind`** classes used,\nplease spend 2 mins Googling \nor searching the official (superb!) docs:\n[tailwindcss.com/docs](https://tailwindcss.com/docs) \nand then if you're still stuck, please\n[open an issue](https://github.com/dwyl/learn-tailwind/issues).\n\nYour `home.html.heex` template file should look like this:\n[`/lib/chat_web/controllers/page_html/home.html.heex`](https://github.com/dwyl/phoenix-chat-example/blob/6d070dd27a69572cca6e35f0703aa535c0201a3c/lib/chat_web/controllers/page_html/home.html.heex)\n\n\n### 3.1 Update Layout Template\n\nOpen the `lib/chat_web/components/layouts/root.html.heex` file\nand locate the `\u003cbody\u003e` tag.\nReplace the contents of the `\u003cbody\u003e` with the following code:\n\n```html\n  \u003cbody class=\"bg-white antialiased min-h-screen flex flex-col\"\u003e\n    \u003cheader class=\"bg-slate-800 w-full h-[4rem] top-0 fixed flex flex-col justify-center z-10\"\u003e\n      \u003cdiv class=\"flex flex-row justify-center items-center\"\u003e\n        \u003ch1 class=\"w-4/5 md:text-3xl text-center font-mono text-white\"\u003e\n          Phoenix Chat Example\n        \u003c/h1\u003e\n      \u003c/div\u003e\n    \u003c/header\u003e\n    \u003c%= @inner_content %\u003e\n  \u003c/body\u003e\n```\n\nYour `root.html.heex` template file should look like this:\n[`/lib/chat_web/components/layouts/root.html.heex`](https://github.com/dwyl/phoenix-chat-example/blob/6d070dd27a69572cca6e35f0703aa535c0201a3c/lib/chat_web/components/layouts/root.html.heex)\n\nAt the end of this step, if you run the Phoenix Server `mix phx.server`,\nand view the App in your browser it will look like this:\n\n![phoenix-chat-blank](https://user-images.githubusercontent.com/17494745/216590189-95923e9a-0956-4468-be8b-63b986d32f14.png)\n\nSo it's already starting to look like a basic Chat App.\nSadly, since we changed the copy of the `home.html.heex`\nour `page_controller_test.exs` now fails:\n\nRun the command:\n\n```sh\nmix test\n```\n\n```\n1) test GET / (ChatWeb.PageControllerTest)\n     test/chat_web/controllers/page_controller_test.exs:4\n     Assertion with =~ failed\n     code:  assert html_response(conn, 200) =~ \"Peace of mind from prototype to production\"\n```\n\nThankfully this is easy to fix.\n\n\n### 3.2 Update the `page_controller_test.exs`\n\nOpen the `test/chat_web/controllers/page_controller_test.exs` file\nand replace the line:\n\n```elixir\n    assert html_response(conn, 200) =~ \"Peace of mind from prototype to production\"\n```\n\nWith:\n\n```elixir\n    assert html_response(conn, 200) =~ \"Phoenix Chat Example\"\n```\n\nNow if you run the tests again, they will pass:\n```\nmix test\n```\n\nSample output:\n\n```\n........\nFinished in 0.1 seconds (0.09s async, 0.06s sync)\n8 tests, 0 failures\n\nRandomized with seed 275786\n```\n\n\u003cbr /\u003e\n\n## 4. Update the \"Client\" code in App.js\n\nOpen\n`assets/js/app.js`,\nuncomment and change the line:\n\n```js\nimport socket from \"./user_socket.js\"\n```\n\nWith the line _uncommented_,\nour app will import the `socket.js` file\nwhich will give us WebSocket functionality.\n\nThen add the following JavaScript (\"Client\") code\nto the bottom of the file:\n\n```js\n/* Message list code */\nconst ul = document.getElementById('msg-list');    // list of messages.\nconst name = document.getElementById('name');      // name of message sender\nconst msg = document.getElementById('msg');        // message input field\nconst send = document.getElementById('send');      // send button\n\nconst channel = socket.channel('room:lobby', {});  // connect to chat \"room\"\nchannel.join(); // join the channel.\n\n// Listening to 'shout' events\nchannel.on('shout', function (payload) {\n  render_message(payload)\n});\n\n\n// Send the message to the server on \"shout\" channel\nfunction sendMessage() {\n\n  channel.push('shout', {        \n    name: name.value || \"guest\", // get value of \"name\" of person sending the message. Set guest as default\n    message: msg.value,          // get message text (value) from msg input field.\n    inserted_at: new Date()      // date + time of when the message was sent\n  });\n\n  msg.value = '';                // reset the message input field for next message.\n  window.scrollTo(0, document.documentElement.scrollHeight) // scroll to the end of the page on send\n}\n\n// Render the message with Tailwind styles\nfunction render_message(payload) {\n\n  const li = document.createElement(\"li\"); // create new list item DOM element\n\n  // Message HTML with Tailwind CSS Classes for layout/style:\n  li.innerHTML = `\n  \u003cdiv class=\"flex flex-row w-[95%] mx-2 border-b-[1px] border-slate-300 py-2\"\u003e\n    \u003cdiv class=\"text-left w-1/5 font-semibold text-slate-800 break-words\"\u003e\n      ${payload.name}\n      \u003cdiv class=\"text-xs mr-1\"\u003e\n        \u003cspan class=\"font-thin\"\u003e${formatDate(payload.inserted_at)}\u003c/span\u003e \n        \u003cspan\u003e${formatTime(payload.inserted_at)}\u003c/span\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n    \u003cdiv class=\"flex w-3/5 mx-1 grow\"\u003e\n      ${payload.message}\n    \u003c/div\u003e\n  \u003c/div\u003e\n  `\n  // Append to list\n  ul.appendChild(li);\n}\n\n// Listen for the [Enter] keypress event to send a message:\nmsg.addEventListener('keypress', function (event) {\n  if (event.key === `Enter` \u0026\u0026 msg.value.length \u003e 0) { // don't sent empty msg.\n    sendMessage()\n  }\n});\n\n// On \"Send\" button press\nsend.addEventListener('click', function (event) {\n  if (msg.value.length \u003e 0) { // don't sent empty msg.\n    sendMessage()\n  }\n});\n\n// Date formatting\nfunction formatDate(datetime) {\n  const m = new Date(datetime);\n  return m.getUTCFullYear() + \"/\" \n    + (\"0\" + (m.getUTCMonth()+1)).slice(-2) + \"/\" \n    + (\"0\" + m.getUTCDate()).slice(-2);\n}\n\n// Time formatting\nfunction formatTime(datetime) {\n  const m = new Date(datetime);\n  return (\"0\" + m.getUTCHours()).slice(-2) + \":\"\n    + (\"0\" + m.getUTCMinutes()).slice(-2) + \":\"\n    + (\"0\" + m.getUTCSeconds()).slice(-2);\n}\n```\n\n\u003e Take a moment to read the JavaScript code\nand confirm your understanding of what it's doing. \u003cbr /\u003e\nHopefully the in-line comments are self-explanatory,\nbut if _anything_ is unclear, please ask!\n\nAt this point your `app.js` file should look like this:\n[`/assets/js/app.js`](https://github.com/dwyl/phoenix-chat-example/blob/f45afee52570e07d43b7e3652564d24857a32bd7/assets/js/app.js)\n\n\n### 4.1 Comment Out Lines in `user_socket.js`\n\nBy default the phoenix channel (client)\nwill subscribe to the generic room: `\"topic:subtopic\"`.\nSince we aren't going to be using this,\nwe can avoid seeing any\n**`\"unable to join: unmatched topic\"`** errors in our browser/console\nby simply commenting out a few lines in the `user_socket.js` file.\nOpen the file in your editor and locate the following lines:\n\n```JavaScript\nlet channel = socket.channel(\"room:42\", {})\nchannel.join()\n  .receive(\"ok\", resp =\u003e { console.log(\"Joined successfully\", resp) })\n  .receive(\"error\", resp =\u003e { console.log(\"Unable to join\", resp) })\n```\nComment out the lines so they will not be executed:\n\n```JavaScript\n//let channel = socket.channel(\"room:42\", {})\n//channel.join()\n//  .receive(\"ok\", resp =\u003e { console.log(\"Joined successfully\", resp) })\n//  .receive(\"error\", resp =\u003e { console.log(\"Unable to join\", resp) })\n```\n\nYour `user_socket.js` should now look like this:\n[`/assets/js/user_socket.js`](https://github.com/dwyl/phoenix-chat-example/blob/f45afee52570e07d43b7e3652564d24857a32bd7/assets/js/user_socket.js)\n\n\u003e If you later decide to tidy up your chat app, you can **`delete`**\nthese commented lines from the file. \u003cbr /\u003e\nWe are just keeping them for reference\nof how to join channels and receive messages.\n\nIf you are running the app,\ntry to fill the `name` and `message` fields\nand click `Enter` (or press `Send`).\n\nThe message should appear\non different windows!\n\n\u003cimg width=\"905\" alt=\"ephemeral_chat\" src=\"https://user-images.githubusercontent.com/17494745/216594102-f39af9c2-25c2-45f0-97bb-feecc434be5a.png\"\u003e\n\nWith this done, we can proceed.\n\n\u003cbr /\u003e\n\n\n### Storing Chat Message Data/History\n\nIf we didn't _want_ to _save_ the chat history,\nwe could just _deploy_ this App _immediately_\nand we'd be done! \u003cbr /\u003e\n\n\u003e In fact, it could be a \"_use-case_\" / \"_feature_\"\nto have \"_ephemeral_\" chat without _any_ history ...\n\u003e see: http://www.psstchat.com/.\n![psst-chat](https://user-images.githubusercontent.com/194400/35284714-6e338596-0053-11e8-998a-83b917ec90ae.png)\n\u003e \n\u003e But we are _assuming_ that _most_ chat apps save history\n\u003e so that `new` people joining the \"channel\" can see the history\n\u003e and people who are briefly \"absent\" can \"catch up\" on the history.\n\n\u003cbr /\u003e\n\n## 5. Generate Database Schema to Store Chat History\n\nRun the following command in your terminal:\n```sh\nmix phx.gen.schema Message messages name:string message:string\n```\nYou should see the following output:\n```sh\n* creating lib/chat/message.ex\n* creating priv/repo/migrations/20230203114114_create_messages.exs\n\nRemember to update your repository by running migrations:\n\n    $ mix ecto.migrate\n```\n\nLet's break down that command for clarity:\n+ `mix phx.gen.schema` - the mix command to create a new schema (database table)\n+ `Message` - the singular name for record in our messages \"collection\"\n+ `messages` - the name of the collection (_or database table_)\n+ `name:string` - the name of the person sending a message, stored as a `string`.\n+ `message:string` - the message sent by the person, also stored as a `string`.\n\nThe line `creating lib/chat/message.ex` creates the \"schema\"\nfor our Message database table.\n\nAdditionally a migration file is created, e.g:\n`creating priv/repo/migrations/20230203114114_create_messages.exs`\nThe \"_migration_\" actually _creates_ the database table in our database.\n\n\u003cbr /\u003e\n\n## 6. Run the Ecto Migration (_Create The Database Table_)\n\nIn your terminal run the following command to create the `messages` table:\n\n```sh\nmix ecto.migrate\n```\n\n\u003e For _context_  we recommend reading:\n[hexdocs.pm/ecto_sql/**Ecto.Migration**.html](https://hexdocs.pm/ecto_sql/Ecto.Migration.html)\n\nYou should see the following in your terminal:\n```sh\n11:42:10.130 [info] == Running 20230203114114 Chat.Repo.Migrations.CreateMessages.change/0 forward\n\n11:42:10.137 [info] create table messages\n\n11:42:10.144 [info] == Migrated 20230203114114 in 0.0s\n```\n\n\u003cbr /\u003e\n\n### 6.1 Review the Messages Table Schema\n\nIf you open your PostgreSQL GUI (_e.g: [pgadmin](https://www.pgadmin.org)_)\nyou will see that the messages table has been created\nin the `chat_dev` database:\n\n![pgadmin-messages-table](https://user-images.githubusercontent.com/194400/35624169-deaa7fd4-0696-11e8-8dd0-584eba3a2037.png)\n\nYou can view the table schema by \"_right-clicking_\" (_`ctrl + click` on Mac_)\non the `messages` table and selecting \"properties\":\n\n![pgadmin-messages-schema-columns-view](https://user-images.githubusercontent.com/194400/35623295-c3a4df5c-0693-11e8-8484-199c2bcab458.png)\n\n\u003e _**Note**: For sections 7, 8, and 9 we will be fleshing out how our code\n\u003e \"handles\" the different events that can occur in our chat app._\n\u003e\n\u003e _Phoenix abstracts away much of the underlying message-passing logic in\n\u003e Elixir's process communication (for more info on how Elixir processes\n\u003e communicate, read [here](https://hexdocs.pm/elixir/processes.html))._\n\u003e\n\u003e _In Phoenix, events/messages sent from the client are automatically\n\u003e routed to the corresponding handler functions based on the event name,\n\u003e making message handling seamless and straightforward!._\n\n\u003cbr /\u003e\n\n## 7. Insert Messages into Database\n\nOpen the `lib/chat_web/channels/room_channel.ex` file\nand inside the function `def handle_in(\"shout\", payload, socket) do`\nadd the following line:\n```elixir\nChat.Message.changeset(%Chat.Message{}, payload) |\u003e Chat.Repo.insert  \n```\n\nSo that your function ends up looking like this:\n```elixir\ndef handle_in(\"shout\", payload, socket) do\n  Chat.Message.changeset(%Chat.Message{}, payload) |\u003e Chat.Repo.insert  \n  broadcast socket, \"shout\", payload\n  {:noreply, socket}\nend\n```\n\nIf you noticed earlier, in our `assets/js/app.js` file, we used the function \n`sendMessage()` to *push* our message to the server on the \"shout\" event. \n\nPhoenix routes the message to the server-side\n`handle_in(\"shout\", payload, socket)` function because the event name \nmatches 'shout'. \n\nIn this function, we handle the payload (which is the message text and\nany other data) and insert it into our database. _Neat!_\n\n\u003cbr /\u003e\n\n\n## 8. Load _Existing_ Messages (_When Someone Joins the Chat_)\n\nOpen the `lib/chat/message.ex` file and import `Ecto.Query`:\n\n```elixir\ndefmodule Chat.Message do\n  use Ecto.Schema\n  import Ecto.Changeset\n  import Ecto.Query # add Ecto.Query\n\n```\n\nThen add a new function to it:\n\n```elixir\ndef get_messages(limit \\\\ 20) do\n  Chat.Message\n  |\u003e limit(^limit)\n  |\u003e order_by(desc: :inserted_at)\n  |\u003e Chat.Repo.all()\nend\n```\nThis function accepts a single parameter `limit` to only return a fixed/maximum\nnumber of records.\nIt uses Ecto's `all` function to fetch all records from the database.\n`Message` is the name of the schema/table we want to get records for,\nand limit is the maximum number of records to fetch.\n\n\u003cbr /\u003e\n\n## 9. Send Existing Messages to the Client when they Join\n\nIn the `/lib/chat_web/channels/room_channel.ex` file create a new function:\n```elixir\n@impl true\ndef handle_info(:after_join, socket) do\n  Chat.Message.get_messages()\n  |\u003e Enum.reverse() # revers to display the latest message at the bottom of the page\n  |\u003e Enum.each(fn msg -\u003e push(socket, \"shout\", %{\n      name: msg.name,\n      message: msg.message,\n      inserted_at: msg.inserted_at,\n    }) end)\n  {:noreply, socket} # :noreply\nend\n```\n\nand at the top of the file update the `join` function to the following:\n\n```elixir\ndef join(\"room:lobby\", payload, socket) do\n  if authorized?(payload) do\n    send(self(), :after_join)\n    {:ok, socket}\n  else\n    {:error, %{reason: \"unauthorized\"}}\n  end\nend\n```\n\n\u003e _**Note**: like section 7, Phoenix knows to call this function when the server\n\u003e sends the internal message `:after_join` via the channel process._\n\u003e\n\u003e _Our `join/3` function in `lib/chat_web/channels/room_channel.ex` sends\n\u003e that `:after_join message` to the channel process when the client successfully\n\u003e connects to the `\"room:lobby\"` topic._\n\n\u003cbr /\u003e\n\n\n## 10. _Checkpoint_: Our Chat App Saves Messages!! (_Try it_!)\n\nStart the Phoenix server (_if it is not already running_):\n```sh\nmix phx.server\n```\n\n\u003e _**Note**: it will take a few seconds to **compile**_.\n\n\nIn your terminal, you should see:\n```sh\n[info] Running ChatWeb.Endpoint with cowboy 2.8.0 at 0.0.0.0:4000 (http)\n[info] Access ChatWeb.Endpoint at http://localhost:4000\n\nwebpack is watching the files…\n```\n\nThis tells us that our code compiled (_as expected_) and the Chat App\nis running on TCP Port `4000`!\n\n**Open** the Chat web app in\n**two _separate_ browser windows**: http://localhost:4000 \u003cbr /\u003e\n(_if your machine only has one browser try using one \"incognito\" tab_)\n\nYou should be able to send messages between the two browser windows: \u003cbr /\u003e\n![phoenix-chat-example-basic-cropped](https://user-images.githubusercontent.com/17494745/216617288-31ab0fbf-9b0e-456f-995a-bfb8499e8847.gif)\n\nCongratulations! You have a _working_ (_basic_) Chat App written in Phoenix!\n\nThe chat (message) history is _saved_!\n\nThis means you can _refresh_ the browser\n_or_ join in a different browser and you will still see the history!\n\n\u003cbr /\u003e\n\n# Testing our App (_Automated Testing_)\n\nAutomated testing is one of the _best_ ways to ensure _reliability_\nin your web applications.\n\n\u003e _**Note**: If you are completely new to Automated Testing\nor \"Test Driven Development\" (\"TDD\"),\nwe recommend reading/following the \"basic\" tutorial:_\n[github.com/dwyl/**learn-tdd**](https://github.com/dwyl/learn-tdd)\n\nTesting in Phoenix is fast (_tests run in parallel!_)\nand easy to get started!\nThe `ExUnit` testing framework is _built-in_\nso there aren't an \"decisions/debates\"\nabout which framework or style to use.\n\nIf you have never seen or written a test with `ExUnit`,\ndon't fear, the syntax should be _familiar_ if you have\nwritten _any_ sort of automated test in the past.\n\n\u003cbr /\u003e\n\n## 11. Run the Default/Generated Tests\n\nWhenever you create a new Phoenix app\nor add a new feature (_like a channel_),\nPhoenix _generates_ a new test for you.\n\nWe _run_ the tests using the **`mix test`** command:\n\n```elixir\n........\nFinished in 0.1 seconds (0.05s async, 0.06s sync)\n8 tests, 0 failures\n\nRandomized with seed 157426\n```\n\nIn this case _none_ of these tests fails. (_8 tests, **0 failure**_)\n\n\n## 12. Understanding The Channel Tests\n\nIt's worth taking a moment (_or as long as you need_!)\nto _understand_ what is going on in the\n[`/room_channel_test.exs`](/test/chat_web/channels/room_channel_test.exs)\nfile. _Open_ it if you have not already, read the test descriptions \u0026 code.\n\n\u003e For a bit of _context_ we recommend reading:\n[https://hexdocs.pm/phoenix/**testing_channels**.html](https://hexdocs.pm/phoenix/testing_channels.html)\n\n### 12.1 _Analyse_ a Test\n\nLet's take a look at the _first_ test in\n[/test/chat_web/channels/room_channel_test.exs](/test/chat_web/channels/room_channel_test.exs#L14-L17):\n\n```elixir\ntest \"ping replies with status ok\", %{socket: socket} do\n  ref = push socket, \"ping\", %{\"hello\" =\u003e \"there\"}\n  assert_reply ref, :ok, %{\"hello\" =\u003e \"there\"}\nend\n```\nThe test gets the `socket` from the `setup` function (_on line 6 of the file_)\nand assigns the result of calling the `push` function to a variable `ref`\n`push` merely _pushes_ a message (_the map `%{\"hello\" =\u003e \"there\"}`_)\non the `socket` to the `\"ping\"` ***topic***.\n\nThe [`handle_in`](https://github.com/nelsonic/phoenix-chat-example/blob/f3823e64d9f9826db67f5cdf228ea5c974ad59fa/lib/chat_web/channels/room_channel.ex#L12-L16)\nfunction clause which handles the `\"ping\"` topic:\n\n```elixir\ndef handle_in(\"ping\", payload, socket) do\n  {:reply, {:ok, payload}, socket}\nend\n```\nSimply _replies_ with the payload you send it,\ntherefore in our _test_ we can use the `assert_reply` Macro\nto assert that the `ref` is equal to `:ok, %{\"hello\" =\u003e \"there\"}`\n\n\u003e _**Note**: if you have questions or need **any** help\nunderstanding the other tests, please open an issue on GitHub\nwe are happy to expand this further!_ \u003cbr /\u003e\n(_we are just trying to keep this tutorial reasonably \"brief\"\nso beginners are not \"overwhelmed\" by anything...)_\n\n\u003cbr /\u003e\n\n## 13. What is _Not_ Tested?\n\n_Often_ we can learn a _lot_ about an application (_or API_)\nfrom reading the tests and seeing where the \"gaps\" in testing are.\n\n_Thankfully_ we can achieve this with only a couple of steps:\n\n\u003cbr /\u003e\n\n### 13.1 Add `excoveralls` as a (Development) Dependency to `mix.exs`\n\nOpen your `mix.exs` file and find the \"deps\" function:\n```elixir\ndefp deps do\n```\n\nAdd a comma to the end of the last line, then add the following line to the end\nof the List:\n```elixir\n{:excoveralls, \"~\u003e 0.15.2\", only: [:test, :dev]} # tracking test coverage\n```\n\nAdditionally, find the `def project do` section (_towards the top of `mix.exs`_)\nand add the following lines to the List:\n\n```elixir\ntest_coverage: [tool: ExCoveralls],\npreferred_cli_env: [\n  coveralls: :test,\n  \"coveralls.detail\": :test,\n  \"coveralls.post\": :test,\n  \"coveralls.html\": :test\n]\n```\n\n_Then_, ***install*** the dependency on `excoveralls`\nwe just added to `mix.exs`:\n\n```sh\nmix deps.get\n```\n\nYou should see:\n\n```sh\nResolving Hex dependencies...\nDependency resolution completed:\n* Getting excoveralls (Hex package)\n... etc.\n```\n\n### 13.2 Create a _New File_ Called `coveralls.json`\n\nIn the \"root\" (_base directory_) of the Chat project,\ncreate a new file called `coveralls.json` and _copy-paste_ the following:\n\n```json\n{\n  \"coverage_options\": {\n    \"minimum_coverage\": 100\n  },\n  \"skip_files\": [\n    \"test/\",\n    \"lib/chat/application.ex\",\n    \"lib/chat_web.ex\",\n    \"lib/chat_web/telemetry.ex\",\n    \"lib/chat_web/components/core_components.ex\",\n    \"lib/chat_web/channels/user_socket.ex\"\n  ]\n}\n\n```\nThis file is quite basic, it instructs the `coveralls` app\nto require a **`minimum_coverage`** of **100%**\n(_i.e. **everything is tested**\u003csup\u003e1\u003c/sup\u003e_)\nand to _ignore_ the files in the `test/` directory for coverage checking.\nWe also ignore files such as `application.ex`,\n`telemetry.ex`, `core_components.ex` and `user_socket.ex`\nbecause they are not relevant for the functionality of our project.\n\n\u003e \u003csmall\u003e_\u003csup\u003e1\u003c/sup\u003eWe believe that **investing**\na little **time up-front** to write tests for **all** our **code**\nis **worth it** to have **fewer bugs** later. \u003cbr /\u003e\n**Bugs** are **expensive**, **tests** are **cheap**\nand **confidence**/**reliability** is **priceless**_. \u003c/small\u003e\n\n\n### 13.3 Run the Tests with Coverage Checking\n\nTo run the tests with coverage, copy-paste the following command\ninto your terminal:\n\n```elixir\nMIX_ENV=test mix do coveralls.json\n```\n\u003e For windows use:\n\u003e ```elixir\n\u003e $env:MIX_ENV=\"test\"; mix do coveralls.json\n\u003e ```\n\nYou should see: \u003cbr /\u003e\n\n```\nRandomized with seed 527109\n----------------\nCOV    FILE                                        LINES RELEVANT   MISSED\n100.0% lib/chat.ex                                     9        0        0\n100.0% lib/chat/message.ex                            26        4        0\n100.0% lib/chat/repo.ex                                5        0        0\n 70.0% lib/chat_web/channels/room_channel.ex          46       10        3\n100.0% lib/chat_web/components/layouts.ex              5        0        0\n100.0% lib/chat_web/controllers/error_html.ex         19        1        0\n100.0% lib/chat_web/controllers/error_json.ex         15        1        0\n100.0% lib/chat_web/controllers/page_controller        9        1        0\n100.0% lib/chat_web/controllers/page_html.ex           5        0        0\n100.0% lib/chat_web/endpoint.ex                       49        0        0\n 66.7% lib/chat_web/router.ex                         27        3        1\n[TOTAL]  80.0%\n----------------\n```\n\nAs we can se here, only **80%** of lines of code in `/lib`\nare being \"covered\" by the tests we have written.\n\nTo **view** the coverage in a web browser run the following:\n\n```elixir\nMIX_ENV=test mix coveralls.html ; open cover/excoveralls.html\n```\n\n\u003cbr /\u003e\n\nThis will open the Coverage Report (HTML) in your default Web Browser: \u003cbr /\u003e\n\n![coverage-80-percent](https://user-images.githubusercontent.com/17494745/216605436-45956f51-8bc1-41ce-b13e-8926364bd419.png)\n\n\n\u003c!-- I think I'm at a point where I need to take a \"Detour\"\nto write up my **Definitive** thoughts on \"Test Coverage\" once-and-for-all! --\u003e\n\n\n### 13.4 Write a Test for the Untested Function\n\nOpen the `test/chat_web/channels/room_channel_test.exs` file\nand add the following test:\n\n```elixir\ntest \":after_join sends all existing messages\", %{socket: socket} do\n  # insert a new message to send in the :after_join\n  payload = %{name: \"Alex\", message: \"test\"}\n  Chat.Message.changeset(%Chat.Message{}, payload) |\u003e Chat.Repo.insert()\n\n  {:ok, _, socket2} = ChatWeb.UserSocket\n    |\u003e socket(\"person_id\", %{some: :assign})\n    |\u003e subscribe_and_join(ChatWeb.RoomChannel, \"room:lobby\")\n\n  assert socket2.join_ref != socket.join_ref\nend\n```\n\nFinally, inside `lib/chat_web/router.ex`,\ncomment the following piece of code.\n\n```elixir\n  pipeline :api do\n    plug :accepts, [\"json\"]\n  end\n```\n\nSince we are not using this `:api` in this project,\nthere is no need to test it.\n\nNow when you run `MIX_ENV=test mix do coveralls.json`\nyou should see:\n\n```\nRandomized with seed 15920\n----------------\nCOV    FILE                                        LINES RELEVANT   MISSED\n100.0% lib/chat.ex                                     9        0        0\n100.0% lib/chat/message.ex                            26        4        0\n100.0% lib/chat/repo.ex                                5        0        0\n100.0% lib/chat_web/channels/room_channel.ex          46       10        0\n100.0% lib/chat_web/components/layouts.ex              5        0        0\n100.0% lib/chat_web/controllers/error_html.ex         19        1        0\n100.0% lib/chat_web/controllers/error_json.ex         15        1        0\n100.0% lib/chat_web/controllers/page_controller        9        1        0\n100.0% lib/chat_web/controllers/page_html.ex           5        0        0\n100.0% lib/chat_web/endpoint.ex                       49        0        0\n100.0% lib/chat_web/router.ex                         27        2        0\n[TOTAL] 100.0%\n----------------\n```\n\nThis test just creates a message before\nthe `subscribe_and_join` so there is a message in the database\nto send out to any clien that joins the chat.\n\nThat way the `:after_join` has at least one message\nand the `Enum.each` will be invoked at least once.\n\nWith that our app is fully tested!\n\n\u003cbr /\u003e\n\n\n# Authentication\n\nWe can *extend* this project\nto support basic authentication.\nIf you want to _understand_ \nhow Authentication is implemented the _easy/fast_ way,\nsee:\n[auth.md](https://github.com/dwyl/phoenix-chat-example/blob/main/auth.md)\n\n\n\u003cbr /\u003e\n\n# Adding `Presence` to track who's online\n\nOne of the great advantages\nof using `Phoenix`\nis that you can \n*easily track processes*\nand channels.\n\nThis paves the way to *effortlessly*\nshowing who's online or not!\n\nIf you are interested in \ndeveloping this feature,\nwe have created a guide in \n[`presence.md`](./presence.md)\njust for you! 😀\n\n\u003cbr /\u003e\n\n# Continuous Integration\n\nContinuous integration \nlets you _automate_ running the tests\nto check/confirm that your app \nis working as _expected_ (_before deploying_).\nThis prevents accidentally \"_breaking_\" your app.\n\n_Thankfully_ the steps are quite simple.\n\nFor an example `ci.yml`, see:\n\n[`.github/workflows/ci.yml`](https://github.com/dwyl/phoenix-chat-example/blob/main/.github/workflows/ci.yml)\n\n\u003cbr /\u003e\n\n# Deployment!\n\nDeployment to Fly.io takes a couple of minutes,\nwe recommend following the official guide:\n[fly.io/docs/elixir/**getting-started**](https://fly.io/docs/elixir/getting-started/)\n\nOnce you have _deployed_ you will will be able\nto view/use your app in any Web/Mobile Browser.\n\ne.g:\n[**phoenix-chat**.fly.dev/](https://phoenix-chat.fly.dev/) \u003cbr /\u003e\n\n\u003cbr /\u003e\n\n\n![thats-all-folks](https://user-images.githubusercontent.com/194400/36492991-6bc5dd42-1726-11e8-9d7b-a11c44d786a0.jpg)\n\n\u003cbr /\u003e\n\n## What _Next_?\n\nIf you found this example useful, please ⭐️ the GitHub repository\nso we (_and others_) know you liked it!\n\nIf you want to learn more Phoenix and the magic of **`LiveView`**,\nconsider reading our beginner's tutorial:\n[github.com/dwyl/**phoenix-liveview-counter-tutorial**](https://github.com/dwyl/phoenix-liveview-counter-tutorial)\n\nFor a version of a chat application using **LiveView** you can read the following repository:\n[github.com/dwyl/**phoenix-liveview-chat-example**](https://github.com/dwyl/phoenix-liveview-chat-example)\n\nThank you for learning with us! ☀️\n\n\n\u003cbr /\u003e \u003cbr /\u003e\n\n\n## Inspiration\n\nThis repo is inspired by @chrismccord's Simple Chat Example:\nhttps://github.com/chrismccord/phoenix_chat_example ❤️\n\nAt the time of writing Chris' example was last updated on\n[20 Feb 2018](https://github.com/chrismccord/phoenix_chat_example/commit/7fb1d3d040b9d1e9a1bbd239c60ca1f4dd403c24)\nand uses\n[Phoenix 1.3](https://github.com/chrismccord/phoenix_chat_example/blob/7fb1d3d040b9d1e9a1bbd239c60ca1f4dd403c24/mix.exs#L25)\nsee:\n[issues/40](https://github.com/chrismccord/phoenix_chat_example/issues/40). \u003cbr /\u003e\nThere are quite a few differences (breaking changes)\nbetween Phoenix 1.3 and 1.6 (_the latest version_). \u003cbr /\u003e\n\nOur tutorial uses Phoenix `1.6.2` (latest as of October 2021).\nOur hope is that by writing (_and maintaining_)\na step-by-step beginner focussed\ntutorial we contribute to the Elixir/Phoenix community\nwithout piling up\n[PRs](https://github.com/chrismccord/phoenix_chat_example/pulls)\non Chris's repo.\n\n\n## Recommended Reading / Learning\n\n+ ExUnit docs: https://hexdocs.pm/ex_unit/ExUnit.html\n+ Testing Phoenix Channels:\nhttps://quickleft.com/blog/testing-phoenix-websockets\n+ Phoenix WebSockets Under a Microscope:\nhttps://zorbash.com/post/phoenix-websockets-under-a-microscope\n","funding_links":[],"categories":["Examples and funny stuff","Elixir"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdwyl%2Fphoenix-chat-example","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdwyl%2Fphoenix-chat-example","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdwyl%2Fphoenix-chat-example/lists"}