- 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
|
||||||
|
|
||||||
|
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
|
## 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
|
### Running
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
@ -10,6 +55,20 @@ npm run dev
|
|||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
|
Run unit tests
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
npm run test
|
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 { cssBundleHref } from "@remix-run/css-bundle";
|
||||||
import type { LinksFunction } from "@remix-run/node";
|
import type { LinksFunction, V2_MetaFunction } from "@remix-run/node";
|
||||||
import { V2_MetaFunction } from "@remix-run/node";
|
|
||||||
import {
|
import {
|
||||||
isRouteErrorResponse,
|
isRouteErrorResponse,
|
||||||
Links,
|
Links,
|
||||||
|
@ -1,16 +1,5 @@
|
|||||||
export default function Index() {
|
import { redirect } from "@remix-run/node";
|
||||||
return (
|
|
||||||
<div className="hero bg-base-200">
|
export function loader() {
|
||||||
<div className="hero-content text-center">
|
return redirect("/listen");
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ export const loader = async ({ request }: LoaderArgs) => {
|
|||||||
prisma.user.count(),
|
prisma.user.count(),
|
||||||
fetch(url.toString(), { method: "HEAD" }).then((r) => {
|
fetch(url.toString(), { method: "HEAD" }).then((r) => {
|
||||||
if (!r.ok) return Promise.reject(r);
|
if (!r.ok) return Promise.reject(r);
|
||||||
}),
|
})
|
||||||
]);
|
]);
|
||||||
return new Response("OK");
|
return new Response("OK");
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
@ -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", () => {
|
|
||||||
|
describe("utils", () => {
|
||||||
|
describe("validateEmail", () => {
|
||||||
|
it("should returns false for non-emails", () => {
|
||||||
expect(validateEmail(undefined)).toBe(false);
|
expect(validateEmail(undefined)).toBe(false);
|
||||||
expect(validateEmail(null)).toBe(false);
|
expect(validateEmail(null)).toBe(false);
|
||||||
expect(validateEmail("")).toBe(false);
|
expect(validateEmail("")).toBe(false);
|
||||||
expect(validateEmail("not-an-email")).toBe(false);
|
expect(validateEmail("not-an-email")).toBe(false);
|
||||||
expect(validateEmail("n@")).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 {
|
export function slugify(string: string): string {
|
||||||
const a = "àáâäæãåāăąçćčđďèéêëēėęěğǵḧîïíīįìıİłḿñńǹňôöòóœøōõőṕŕřßśšşșťțûüùúūǘůűųẃẍÿýžźż·/_,:;";
|
const a = "àáâäæãåāăąçćčđďèéêëēėęěğǵḧîïíīįìıİłḿñńǹňôöòóœøōõőṕŕřßśšşșťțûüùúūǘůűųẃẍÿýžźż·/_,:;";
|
||||||
const b = "aaaaaaaaaacccddeeeeeeeegghiiiiiiiilmnnnnoooooooooprrsssssttuuuuuuuuuwxyyzzz------";
|
const b = "aaaaaaaaaacccddeeeeeeeegghiiiiiiiilmnnnnoooooooooprrsssssttuuuuuuuuuwxyyzzz------";
|
||||||
|
@ -15,7 +15,7 @@ describe("smoke tests", () => {
|
|||||||
|
|
||||||
cy.visitAndCheck("/");
|
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.findByRole("textbox", { name: /email/i }).type(loginForm.email);
|
||||||
cy.findByLabelText(/password/i).type(loginForm.password);
|
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