Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor to use local storage #8

Merged
merged 6 commits into from
Jun 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading