Skip to content

Commit

Permalink
Add isbn as parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
Gnuk committed Aug 8, 2024
1 parent 3df0f4e commit b282840
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 80 deletions.
44 changes: 44 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Main

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js LTS
uses: actions/setup-node@v4
with:
cache: 'npm'
node-version: 'lts/*'
- name: node check
run: |
npm ci
npm test
env:
CI: true

status-checks:
name: status-checks
needs: [build]
permissions:
contents: none
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Validation Status checks
run: |
echo 'Configuration for Status checks that are required'
echo '${{ toJSON(needs) }}'
if [[ ('skipped' == '${{ needs.build.result }}') || ('success' == '${{ needs.build.result }}') ]]; then
exit 0
fi
exit 1
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
"type": "module",
"scripts": {
"dev": "vite",
"component:serve": "NODE_ENV=production vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0 --ignore-path .gitignore",
"lint:fix": "npm run lint -- --fix",
"preview": "vite preview",
"test": "vitest",
"test:ci": "vitest run --coverage",
"test:component": "start-server-and-test dev http://localhost:3030 test:component:open",
"test:component": "start-server-and-test component:serve http://localhost:3030 test:component:open",
"test:component:ci": "start-server-and-test dev http://localhost:3030 test:component:run",
"test:component:open": "cypress open --config-file test/component/cypress.config.ts",
"test:component:run": "cypress run --config-file test/component/cypress.config.ts"
Expand Down
12 changes: 9 additions & 3 deletions src/library/application/LibraryRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { RouteObject } from 'react-router-dom';
import { RouteObject, useParams } from 'react-router-dom';
import { BookComponent } from '@/library/infrastructure/primary/BookComponent.tsx';
import { LibraryApp } from '@/library/application/LibraryApp.tsx';
import { ISBN } from '@/library/domain/ISBN.ts';

const BooksPage = () => {
const { isbn } = useParams<string>();
return <BookComponent isbn={ISBN.of(isbn!)} />;
};

export const libraryRoutes: RouteObject = {
path: '/',
element: <LibraryApp />,
children: [
{
path: '',
element: <BookComponent />,
path: 'book/:isbn',
element: <BooksPage />,
},
],
};
3 changes: 2 additions & 1 deletion src/library/domain/Books.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Book } from '@/library/domain/Book';
import { Either } from '@/functional/Either';
import { ISBN } from '@/library/domain/ISBN.ts';

export interface Books {
get(): Promise<Either<Error, Book>>;
get(isbn: ISBN): Promise<Either<Error, Book>>;
}
57 changes: 34 additions & 23 deletions src/library/infrastructure/primary/BookComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,44 @@
import { useState } from 'react';
import { useLoadEither } from '@/library/infrastructure/primary/UseLoad';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { inject } from '@/injections.ts';
import { BOOKS } from '@/library/application/LibraryKeys.ts';
import { Book } from '@/library/domain/Book.ts';
import { ISBN } from '@/library/domain/ISBN.ts';
import { Loader, loadError, loadFor, loadInProgress, loadSuccess } from '@/library/infrastructure/primary/Loader.ts';

export const BookComponent = () => {
const books = inject(BOOKS);
const BookInfoComponent = ({ book }: { book: Book }) => {
const { t } = useTranslation();
return (
<ul>
<li data-selector="book.title">
<strong data-selector="book.label.title">{t('book.title')} </strong>
<span data-selector="book.title">{book.title}</span>
</li>
<li data-selector="book.isbn">{book.isbn.get()}</li>
</ul>
);
};

export const BookComponent = (props: { isbn: ISBN }) => {
const { t } = useTranslation();
const [book, setBook] = useState<Book>();
const [bookLoader, setBookLoader] = useState<Loader<Book>>(loadInProgress());

const { isInProgress, isFailing, isSuccessful, errorMessage } = useLoadEither(books.get(), book => {
setBook(book);
});
useEffect(() => {
setBookLoader(loadInProgress());
inject(BOOKS)
.get(props.isbn)
.then(either =>
either.evaluate(
error => setBookLoader(loadError(error.message)),
content => setBookLoader(loadSuccess(content))
)
)
.catch((error: Error) => setBookLoader(loadError(error.message)));
}, [props.isbn]);

return (
<>
{isInProgress && <p data-selector="book.loading">{t('book.inProgress')}</p>}
{isFailing && <p data-selector="book.error">{errorMessage}</p>}
{isSuccessful && (
<ul>
<li data-selector="book.title">
<strong data-selector="book.label.title">{t('book.title')} </strong>
<span data-selector="book.title">{book?.title}</span>
</li>
<li data-selector="book.isbn">{book?.isbn.get()}</li>
</ul>
)}
</>
);
return loadFor(bookLoader)({
progress: () => <p data-selector="book.loading">{t('book.inProgress')}</p>,
error: message => <p data-selector="book.error">{message}</p>,
success: book => <BookInfoComponent book={book} />,
});
};
58 changes: 58 additions & 0 deletions src/library/infrastructure/primary/Loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ReactElement } from 'react';

const LoadingInProgress = Symbol();
const LoadingError = Symbol();
const LoadingSuccess = Symbol();

type LoadingStatus = typeof LoadingError | typeof LoadingInProgress | typeof LoadingSuccess;

interface LoadWithStatus {
status: LoadingStatus;
}

interface LoadSuccess<T> extends LoadWithStatus {
content: T;
status: typeof LoadingSuccess;
}

interface LoadError extends LoadWithStatus {
errorMessage: string;
status: typeof LoadingError;
}

interface LoadInProgress extends LoadWithStatus {
status: typeof LoadingInProgress;
}

export type Loader<T> = LoadSuccess<T> | LoadError | LoadInProgress;

export const loadInProgress = (): LoadInProgress => ({ status: LoadingInProgress });

export const loadSuccess = <T>(content: T): LoadSuccess<T> => ({
content,
status: LoadingSuccess,
});

export const loadError = (errorMessage: string): LoadError => ({
errorMessage,
status: LoadingError,
});

type LoaderCallback<T> = {
success: (content: T) => ReactElement;
error: (message: string) => ReactElement;
progress: () => ReactElement;
};

export const loadFor =
<T>(loader: Loader<T>) =>
({ success, error, progress }: LoaderCallback<T>): ReactElement => {
switch (loader.status) {
case LoadingInProgress:
return progress();
case LoadingError:
return error(loader.errorMessage);
case LoadingSuccess:
return success(loader.content);
}
};
39 changes: 0 additions & 39 deletions src/library/infrastructure/primary/UseLoad.ts

This file was deleted.

5 changes: 3 additions & 2 deletions src/library/infrastructure/secondary/RestBooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { Either } from '@/functional/Either';
import { Book } from '@/library/domain/Book';
import { AxiosInstance } from 'axios';
import { RestBook, toBook } from '@/library/infrastructure/secondary/RestBook';
import { ISBN } from '@/library/domain/ISBN.ts';

export class RestBooks implements Books {
constructor(private readonly axiosInstance: AxiosInstance) {}

get(): Promise<Either<Error, Book>> {
get(isbn: ISBN): Promise<Either<Error, Book>> {
return this.axiosInstance
.get<RestBook>('/isbn/9780321125217.json')
.get<RestBook>(`/isbn/${isbn.get()}.json`)
.then(response => response.data)
.then(toBook)
.catch(error => Promise.resolve(Either.err(error)));
Expand Down
18 changes: 9 additions & 9 deletions test/component/spec/library/library.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const BOOK = {
number_of_pages: 42,
};

const stubOpenLibraryIsbn = () => cy.intercept('https://openlibrary.org/isbn/9780321125217.json', BOOK);
const stubOpenLibraryIsbn = () => cy.intercept('https://openlibrary.org/isbn/9780321125217.json', BOOK).as('BOOK');

const stubOpenLibraryIsbnInvalid = () =>
cy.intercept('https://openlibrary.org/isbn/9780321125217.json', {
Expand Down Expand Up @@ -34,13 +34,13 @@ describe('Library', () => {
});

it('Should be loading before result', () => {
cy.visit('/', loadLanguage('en'));
cy.visit('/book/9780321125217', loadLanguage('en'));

cy.contains(dataSelector('book.loading'), 'In progress');
});

it('Should be loading before result', () => {
cy.visit('/', loadLanguage('fr'));
cy.visit('/book/9780321125217', loadLanguage('fr'));

cy.contains(dataSelector('book.loading'), 'En cours');
});
Expand All @@ -49,23 +49,23 @@ describe('Library', () => {
it('Should not show book with network error', () => {
stubOpenLibraryIsbnNetworkError();

cy.visit('/');
cy.visit('/book/9780321125217');

cy.contains(dataSelector('book.error'), 'Request failed');
});

it('Should not show book with invalid ISBN', () => {
stubOpenLibraryIsbnInvalid();

cy.visit('/');
cy.visit('/book/9780321125217');

cy.contains(dataSelector('book.error'), 'Non digits are not allowed for ISBN');
});

it('Should get book', () => {
stubOpenLibraryIsbn();

cy.visit('/');
cy.visit('/book/9780321125217');

cy.contains(dataSelector('book.title'), 'Domain-driven design');
cy.contains(dataSelector('book.isbn'), '9780321125217');
Expand All @@ -75,15 +75,15 @@ describe('Library', () => {
it('Should have english labels', () => {
stubOpenLibraryIsbn();

cy.visit('/', loadLanguage('en'));
cy.visit('/book/9780321125217', loadLanguage('en'));

cy.contains(dataSelector('book.label.title'), 'Title: ');
});

it('Should have french labels', () => {
it.only('Should have french labels', () => {
stubOpenLibraryIsbn();

cy.visit('/', loadLanguage('fr'));
cy.visit('/book/9780321125217', loadLanguage('fr'));

cy.contains(dataSelector('book.label.title'), 'Titre : ');
});
Expand Down
6 changes: 4 additions & 2 deletions test/unit/library/infrastructure/secondary/RestBooks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const stubAxiosInstance = (): AxiosInstanceStub =>
get: sinon.stub(),
}) as AxiosInstanceStub;

const FAKE_ISBN = ISBN.of('9780321125217');

describe('RestBooks', () => {
it('Should get book', async () => {
const axiosInstance = stubAxiosInstance();
Expand All @@ -26,7 +28,7 @@ describe('RestBooks', () => {
axiosInstance.get.resolves(response);
const repository = new RestBooks(axiosInstance);

const eitherBook = await repository.get();
const eitherBook = await repository.get(FAKE_ISBN);

expect(axiosInstance.get.getCall(0).args).toContain('/isbn/9780321125217.json');
expect(eitherBook).toEqual<Either<Error, Book>>(
Expand All @@ -43,7 +45,7 @@ describe('RestBooks', () => {
axiosInstance.get.rejects(new Error('Network error'));
const repository = new RestBooks(axiosInstance);

const eitherBook = await repository.get();
const eitherBook = await repository.get(FAKE_ISBN);

expect(eitherBook).toEqual<Either<Error, Book>>(Err.of(new Error('Network error')));
});
Expand Down

0 comments on commit b282840

Please sign in to comment.