- Fixed E2E smoke tests

- Added unit test for `slugify`
- Updated readme
This commit is contained in:
Luke Bunselmeyer 2023-05-08 00:55:06 -04:00
parent e7464e6299
commit 78c07c40cc
8 changed files with 117 additions and 52 deletions

View File

@ -1,7 +1,52 @@
# Awesome Radio
Awesome Radio is a personal internet radio station aggregator.
![Screenshot](screenshot.png)
## Features
* Browse radio stations by tag
* Listen while navigating
* Fully responsive UI for mobile, tablets, and desktop
* Light and dark theme automatically enabled by OS settings
* Deep linking for all UI actions
* User accounts
* Add content sources to import stations
## Roadmap
* Support user favorites
* Support importing from other source types
* Support manually adding/editing/disabling stations
* Support [PWA](https://web.dev/progressive-web-apps/) to allow user to save to home screen on mobile devices
* Fix: Primary drawer stays open after navigation
* Tech Debt: Add more unit and E2E tests
## Development
### Tech Stack
* [Remix](https://remix.run): React SSR web framework
* [SQLite](https://www.sqlite.org): File based relational database
* [Prisma](https://www.prisma.io/): Node TS ORM
* [Vitest](https://vitest.dev): Unit test framework
### Getting Started
1. Create `.env` file from `.env.example`
```shell
cp .env.example .env
```
2. Migrate & Seed the SQLite DB
```shell
npx prisma migrate deploy
npx prisma db seed
```
### Running
```shell
@ -10,6 +55,20 @@ npm run dev
### Testing
Run unit tests
```shell
npm run test
```
Run E2E tests
```shell
npm run test:e2e:run
```
Run all checks
```shell
npm run validate
```

View File

@ -1,6 +1,5 @@
import { cssBundleHref } from "@remix-run/css-bundle";
import type { LinksFunction } from "@remix-run/node";
import { V2_MetaFunction } from "@remix-run/node";
import type { LinksFunction, V2_MetaFunction } from "@remix-run/node";
import {
isRouteErrorResponse,
Links,

View File

@ -1,16 +1,5 @@
export default function Index() {
return (
<div className="hero bg-base-200">
<div className="hero-content text-center">
<div className="max-w-md">
<h1 className="text-5xl font-bold">Hello there</h1>
<p className="py-6">Provident cupiditate voluptatem et in. Quaerat fugiat ut assumenda excepturi
exercitationem quasi. In deleniti eaque aut repudiandae et a id nisi.</p>
<button className="btn btn-primary">
Get Started
</button>
</div>
</div>
</div>
);
import { redirect } from "@remix-run/node";
export function loader() {
return redirect("/listen");
}

View File

@ -4,22 +4,22 @@ import type { LoaderArgs } from "@remix-run/node";
import { prisma } from "~/db.server";
export const loader = async ({ request }: LoaderArgs) => {
const host =
request.headers.get("X-Forwarded-Host") ?? request.headers.get("host");
const host =
request.headers.get("X-Forwarded-Host") ?? request.headers.get("host");
try {
const url = new URL("/", `http://${host}`);
// if we can connect to the database and make a simple query
// and make a HEAD request to ourselves, then we're good.
await Promise.all([
prisma.user.count(),
fetch(url.toString(), { method: "HEAD" }).then((r) => {
if (!r.ok) return Promise.reject(r);
}),
]);
return new Response("OK");
} catch (error: unknown) {
console.log("healthcheck ❌", { error });
return new Response("ERROR", { status: 500 });
}
try {
const url = new URL("/", `http://${host}`);
// if we can connect to the database and make a simple query
// and make a HEAD request to ourselves, then we're good.
await Promise.all([
prisma.user.count(),
fetch(url.toString(), { method: "HEAD" }).then((r) => {
if (!r.ok) return Promise.reject(r);
})
]);
return new Response("OK");
} catch (error: unknown) {
console.log("healthcheck ❌", { error });
return new Response("ERROR", { status: 500 });
}
};

View File

@ -1,13 +1,38 @@
import { validateEmail } from "./utils";
import { describe, expect, it } from "vitest";
import { slugify, validateEmail } from "./utils";
test("validateEmail returns false for non-emails", () => {
expect(validateEmail(undefined)).toBe(false);
expect(validateEmail(null)).toBe(false);
expect(validateEmail("")).toBe(false);
expect(validateEmail("not-an-email")).toBe(false);
expect(validateEmail("n@")).toBe(false);
describe("utils", () => {
describe("validateEmail", () => {
it("should returns false for non-emails", () => {
expect(validateEmail(undefined)).toBe(false);
expect(validateEmail(null)).toBe(false);
expect(validateEmail("")).toBe(false);
expect(validateEmail("not-an-email")).toBe(false);
expect(validateEmail("n@")).toBe(false);
});
it("should returns true for emails", () => {
expect(validateEmail("kody@example.com")).toBe(true);
});
});
describe("slugify", () => {
it("should convert text into url safe text", () => {
expect(slugify("Abc dEf")).toBe("abc-def");
expect(slugify(" abc def ")).toBe("abc-def");
expect(slugify("abcDef")).toBe("abcdef");
expect(slugify("abc.def")).toBe("abcdef");
expect(slugify("abc!def")).toBe("abcdef");
expect(slugify("abcdëf")).toBe("abcdef");
expect(slugify("abc--def")).toBe("abc-def");
expect(slugify("abc&def")).toBe("abc-and-def");
expect(slugify("abc12def")).toBe("abc12def");
expect(slugify("abc_def")).toBe("abc-def");
});
});
});
test("validateEmail returns true for emails", () => {
expect(validateEmail("kody@example.com")).toBe(true);
});

View File

@ -77,13 +77,6 @@ export function notFound(message?: string) {
});
}
export function createIndex<T>(records: T[], keyFn: (t: T) => string): Map<string, T> {
return records.reduce((index, record) => {
index.set(keyFn(record), record);
return index;
}, new Map<string, T>());
}
export function slugify(string: string): string {
const a = "àáâäæãåāăąçćčđďèéêëēėęěğǵḧîïíīįìıİłḿñńǹňôöòóœøōõőṕŕřßśšşșťțûüùúūǘůűųẃẍÿýžźż·/_,:;";
const b = "aaaaaaaaaacccddeeeeeeeegghiiiiiiiilmnnnnoooooooooprrsssssttuuuuuuuuuwxyyzzz------";

View File

@ -15,7 +15,7 @@ describe("smoke tests", () => {
cy.visitAndCheck("/");
cy.findByRole("link", { name: /sign up/i }).click();
cy.findByRole("link", { name: /join/i }).click();
cy.findByRole("textbox", { name: /email/i }).type(loginForm.email);
cy.findByLabelText(/password/i).type(loginForm.password);

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB