Skip to content

Commit

Permalink
Refactor to use local storage (#8)
Browse files Browse the repository at this point in the history
* initial take on refactoring to store and client

* connect section title to store

* make the app use localStorage

* remote IStore interface to simplify. fix workflow test command

* use typed in username for comments

* add import for index.css
  • Loading branch information
mickmister authored Jun 1, 2024
1 parent 571c1b0 commit bc5af78
Show file tree
Hide file tree
Showing 25 changed files with 1,002 additions and 261 deletions.
10 changes: 10 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": [
"react-app",
"react-app/jest"
],
"rules": {
"testing-library/no-container": "off",
"testing-library/no-node-access": "off"
}
}
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,17 @@ jobs:
run: npm i
- name: Run eslint
run: npm run lint

test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 21
cache: 'npm'
- name: Install modules
run: npm i
- name: Run tests
run: npm run test:ci
10 changes: 3 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,12 @@
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"test:ci": "react-scripts test --watchAll=false",
"eject": "react-scripts eject",
"lint": "eslint src/**/*.ts*",
"fix": "npm run lint -- --fix",
"check-types": "tsc --noEmit"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
"check-types": "tsc --noEmit",
"ci": "npm run lint && npm run check-types && npm run test:ci && npm run build"
},
"browserslist": {
"production": [
Expand Down
211 changes: 198 additions & 13 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,201 @@
import { render, screen } from '@testing-library/react';
import * as testData from './sampleData'
import {render, screen, waitFor} from '@testing-library/react';
import App from './App';
import {IClient} from './client/IClient';
import {ProjectData, CommentData, SectionData, EntityType, FileData} from './types';
import {LocalStorageStore, StoreData} from './store/LocalStorageStore';
import {MockLocalStorageDependency} from './store/MockLocalStorageDependency';
import {LocalStorageClient} from './client/LocalStorageClient';

test('renders learn react link', () => {
render(
<App
sectionData={testData.sectionData}
chordProgression={testData.currentChordProgression}
files={testData.files}
comments={testData.comments}
/>
);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
// import * as testData from './sampleData'

window.alert = () => {};

const makeTestStore = (): StoreData => {
const initialProjects: ProjectData[] = [
{
id: 'project-1',
},
{
id: 'project-2',
},
];

const initialSections: SectionData[] = [
{
id: 'section-1',
projectId: 'project-1',
chordProgression: ['C', 'Dm', 'F', 'G'],
description: 'This is the intro',
title: 'Intro',
numRevisions: 3,
}
];

const initialFiles: FileData[] = [
{
id: 'file-1',
projectId: 'project-1',
entityId: 'section-1',
entityType: EntityType.SECTION,
title: 'Bass.mp3',
},
{
id: 'file-2',
projectId: 'project-1',
entityId: 'section-1',
entityType: EntityType.SECTION,
title: 'Chunky Monkey.mp3',
},
];

const initialComments: CommentData[] = [
{
id: 'comment-1',
projectId: 'project-1',
message: 'Hey what\'s up',
entityType: EntityType.SECTION,
entityId: 'section-1',
username: 'username-1',
},
{
id: 'comment-2',
projectId: 'project-1',
message: 'Yeah',
entityType: EntityType.FILE,
entityId: 'file-1',
username: 'username-1',
},
{
id: 'comment-3',
projectId: 'project-1',
message: 'Yeah 3',
entityType: EntityType.FILE,
entityId: 'file-1',
username: 'username-1',
},
];

return {
projects: initialProjects,
sections: initialSections,
files: initialFiles,
comments: initialComments,
};
};

describe('App', () => {
let client: IClient;

beforeEach(() => {
const initialStore = makeTestStore();

const localStorageDependency = new MockLocalStorageDependency(initialStore);
const store = new LocalStorageStore(localStorageDependency);
client = new LocalStorageClient(store);
});

describe('initializing', () => {
it('should show "Loading"', async () => {
// this method is made blocking for this specific test
client.fetchFullDataForProject = (() => new Promise(r => setTimeout(r)));

render(
<App
projectId={'project-1'}
sectionId={'section-1'}
client={client}
/>
);

expect(screen.getByText(/Loading/)).toBeDefined();
});

it('should show client error', async () => {
client.fetchFullDataForProject = jest.fn().mockResolvedValue(new Error('Some error'));

render(
<App
projectId={'project-1'}
sectionId={'section-1'}
client={client}
/>
);

await waitFor(() => {
expect(screen.queryByText(/Loading/)).toBeNull();
});

expect(screen.getByText(/Some error/)).toBeDefined();
});
});

describe('initialized', () => {
it('should show the section title and description', async () => {
render(
<App
projectId={'project-1'}
sectionId={'section-1'}
client={client}
/>
);

await waitFor(() => {
expect(screen.queryByText(/Loading/)).toBeNull();
});

expect(screen.getByText(/Intro/)).toBeDefined();
expect(screen.getByText(/This is the intro/)).toBeDefined();
});

it('should show the chord progression', async () => {
const {container} = render(
<App
projectId={'project-1'}
sectionId={'section-1'}
client={client}
/>
);

await waitFor(() => {
expect(screen.queryByText(/Loading/)).toBeNull();
});

expect(container.querySelector('.chords')?.textContent).toEqual('CDmFG');
});

it('should show files attached to the section', async () => {
const {container} = render(
<App
projectId={'project-1'}
sectionId={'section-1'}
client={client}
/>
);

await waitFor(() => {
expect(screen.queryByText(/Loading/)).toBeNull();
});

expect(container.querySelector('.files #file-1')?.textContent).toContain('Bass.mp3');
expect(container.querySelector('.files #file-1')?.textContent).toContain('2 Comments');
});

it('should show the comments on the section', async () => {
const {container} = render(
<App
projectId={'project-1'}
sectionId={'section-1'}
client={client}
/>
);

await waitFor(() => {
expect(screen.queryByText(/Loading/)).toBeNull();
});

expect(container.querySelector('.comments')?.textContent).toContain('1 Comment');
expect(container.querySelector('.comments #comment-1')?.textContent).toContain('username-1');
expect(container.querySelector('.comments #comment-1')?.textContent).toContain('Hey what\'s up');
});
});
});
79 changes: 55 additions & 24 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,69 @@
import {useState} from 'react';

import './App.css';
import './css_reset.css'
import './index.css'
import './section_view.css';
import * as types from './types';
import { Files } from './Files';
import { ChordProgression } from './ChordProgression';
import { Comments } from './Comments';
import { CreateComment } from './CreateComment';
import { SectionTitle } from './SectionTitle';
import { useState } from 'react';
import {GlobalStoreProvider} from './hooks/useGlobalStore';
import SectionPage from './SectionPage';
import {IClient} from './client/IClient';
import {ClientProvider} from './hooks/useClient';
import {useMount} from './hooks/useMount';

type AppProps = {
projectId: string;
sectionId: string;

client: IClient;
}

const App: React.FC<AppProps> = ({projectId, sectionId, client}) => {
const [initialProjectData, setInitialProjectData] = useState<types.FullProjectData | null>(null);
const [error, setError] = useState('');

type AppProps = {
sectionData: types.SectionData,
chordProgression: types.ChordProgression,
files: types.File[],
comments: types.Comment[]
}
useMount(async () => {
const projectDataOrError = await client.fetchFullDataForProject(projectId);

if (projectDataOrError instanceof Error) {
alert(projectDataOrError.message);
setError(projectDataOrError.message);
return;
}

setInitialProjectData(projectDataOrError);
});

if (error) {
return (
<p>
{error}
</p>
);
}

const App:React.FC<AppProps> = ({sectionData, chordProgression, comments, files}) => {
if (!initialProjectData) {
return (
<p>
Loading
</p>
);
}

const [commentsAsState, setCommentsAsState] = useState<types.Comment[]>(comments)

const pageContent = (
<SectionPage
projectId={projectId}
sectionId={sectionId}
/>
);

return (
<div className="root">
<SectionTitle sectionData={sectionData} />
<ChordProgression chordProgression={chordProgression} />
<Files files={files}/>
<Comments comments={commentsAsState} setComments={setCommentsAsState}/>
<CreateComment comments={commentsAsState} setComments={setCommentsAsState}/>
</div>
);
return (
<ClientProvider client={client}>
<GlobalStoreProvider initialProjectData={initialProjectData}>
{pageContent}
</GlobalStoreProvider>
</ClientProvider>
);
}

export default App;
2 changes: 1 addition & 1 deletion src/ChordProgression.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const ChordProgression: React.FC<ChordProgressionProps> = ({ chordProgres
return (
<div className="chords">
<ol>
{chordProgression.map((chord, index) => <li>{chord}</li>)}
{chordProgression.map((chord, index) => <li key={index}>{chord}</li>)}
</ol>
</div>
);
Expand Down
Loading

0 comments on commit bc5af78

Please sign in to comment.