- Fixed E2E smoke tests
- Added unit test for `slugify` - Updated readme
This commit is contained in:
parent
e7464e6299
commit
78c07c40cc
59
README.md
59
README.md
@ -1,7 +1,52 @@
|
||||
# Awesome Radio
|
||||
|
||||
Awesome Radio is a personal internet radio station aggregator.
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
```
|
||||
|
@ -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,
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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 });
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
@ -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------";
|
||||
|
@ -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
BIN
screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 MiB |
Loading…
x
Reference in New Issue
Block a user