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

✅ Bootstrap Tests #238

Merged
merged 1 commit into from
Feb 27, 2022
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
27 changes: 26 additions & 1 deletion .github/workflows/integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,37 @@ on:
pull_request:

jobs:
test:
name: Test
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/[email protected]

- name: Set up Node
uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'yarn'

- name: Install Dependencies
run: yarn install --frozen-lockfile

- name: Test
run: yarn run test --coverage

- name: Codecov
uses: codecov/[email protected]
with:
token: ${{ secrets.CODECOV_TOKEN }}

build:
name: Build
runs-on: ubuntu-20.04
needs: [test]
strategy:
matrix:
node-version: ['12', '14', '16']
node-version: [ '12', '14', '16' ]

steps:
- name: Checkout
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*

react-app-env.d.ts
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
"typescript": "^4.1.2"
},
"devDependencies": {
"@testing-library/dom": "^8.11.3",
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.3",
"@types/jest": "^27.4.1",
"@typescript-eslint/eslint-plugin": "^4.28.1",
"@typescript-eslint/parser": "^4.22.0",
"eslint": "^7.29.0",
Expand All @@ -39,6 +43,12 @@
"eject": "react-scripts eject",
"lint": "eslint --ext=ts,tsx src"
},
"coverageReporters": [
"clover",
"json",
"lcov",
"text"
],
"browserslist": {
"production": [
">0.2%",
Expand Down
95 changes: 95 additions & 0 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React from 'react'
import App from './App'
import { render, screen } from '@testing-library/react'
import * as api from './lib/api'
import { Settings, Ticker } from './lib/types'

describe('App', function () {
const initSettings = {
refresh_interval: 1000,
inactive_settings: {
author: 'Systemli Ticker Team',
email: '[email protected]',
homepage: '',
twitter: '',
headline: 'The ticker is currently inactive.',
sub_headline: 'Please contact us if you want to use it.',
description: '...',
},
} as Settings
const ticker = {
id: '1',
active: true,
creation_date: new Date(),
title: 'Ticker Title',
description: 'Ticker Description',
domain: 'example.com',
information: {
author: 'Systemli Ticker Team',
url: '',
email: '',
twitter: '',
facebook: '',
},
} as Ticker

test('renders OfflineView', async function () {
jest.spyOn(api, 'getInit').mockRejectedValue(new TypeError())
render(<App />)

expect(screen.getByText('Loading')).toBeInTheDocument()

expect(
await screen.findByText('It seems that you are offline.')
).toBeInTheDocument()
})

test('renders InactiveView', async function () {
jest.spyOn(api, 'getInit').mockResolvedValue({
data: {
settings: initSettings,
ticker: null,
},
})
render(<App />)

expect(screen.getByText('Loading')).toBeInTheDocument()

expect(
await screen.findByText('The ticker is currently inactive.')
).toBeInTheDocument()
})

test('renders ActiveView', async function () {
jest.spyOn(api, 'getInit').mockResolvedValue({
data: {
settings: initSettings,
ticker: ticker,
},
})
jest.spyOn(api, 'getTimeline').mockResolvedValue({
data: {
messages: [],
},
})
const intersectionObserverMock = () => ({
observe: () => null,
})
window.IntersectionObserver = jest
.fn()
.mockImplementation(intersectionObserverMock)
render(<App />)

expect(screen.getByText('Loading')).toBeInTheDocument()

expect(
await screen.findByText(
'The messages update automatically. There is no need to reload the entire page.'
)
).toBeInTheDocument()

expect(
screen.getByText('We dont have any messages at the moment.')
).toBeInTheDocument()
})
})
26 changes: 11 additions & 15 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { FC, useEffect, useState } from 'react'
import { Container, Dimmer, Loader } from 'semantic-ui-react'
// import * as OfflinePluginRuntime from 'offline-plugin/runtime'
import { apiUrl } from './lib/helper'
import { Ticker, Settings } from './lib/types'
import { Settings, Ticker } from './lib/types'
import { ActiveView, ErrorView, InactiveView } from './views'
import { getInit } from './lib/api'

const App: FC = () => {
const [ticker, setTicker] = useState<Ticker | null>(null)
Expand All @@ -22,21 +22,13 @@ const App: FC = () => {
// })

const fetchInit = () => {
const url = `${apiUrl}/init`
fetch(url)
getInit()
.then(response => {
if (!response.ok) {
setGotError(true)
return
}
return response.json()
})
.then(response => {
if (response.data?.settings) {
if (response.data.settings) {
setSettings(response.data.settings)
}

if (response.data?.ticker?.active) {
if (response.data.ticker?.active) {
setTicker(response.data.ticker)
if (ticker?.title) {
document.title = ticker.title
Expand All @@ -45,8 +37,12 @@ const App: FC = () => {

setIsLoading(false)
})
.catch(() => {
setIsOffline(true)
.catch(error => {
if (error instanceof TypeError) {
setIsOffline(true)
} else {
setGotError(true)
}
setIsLoading(false)
})
}
Expand Down
25 changes: 25 additions & 0 deletions src/components/MessageList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react'
import * as api from '../lib/api'
import MessageList from './MessageList'
import { render, screen } from '@testing-library/react'

describe('MessageList', function () {
test('renders empty Messages', async function () {
jest.spyOn(api, 'getTimeline').mockResolvedValue({
data: { messages: [] },
})
const intersectionObserverMock = () => ({
observe: () => null,
})
window.IntersectionObserver = jest
.fn()
.mockImplementation(intersectionObserverMock)

render(<MessageList refreshInterval={10} />)

expect(screen.getByText('Loading messages')).toBeInTheDocument()
expect(
await screen.findByText('We dont have any messages at the moment.')
).toBeInTheDocument()
})
})
22 changes: 9 additions & 13 deletions src/components/MessageList.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { FC, useState, useEffect, useCallback, useRef } from 'react'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { Dimmer, Header, Icon, Loader, Segment } from 'semantic-ui-react'
import { apiUrl } from '../lib/helper'
import { Message as MessageType } from '../lib/types'
import Message from './Message'
import { getTimeline } from '../lib/api'

interface Props {
refreshInterval: number
Expand All @@ -11,21 +11,18 @@ interface Props {
const MessageList: FC<Props> = props => {
const [isLoading, setIsLoading] = useState<boolean>(true)
const [messages, setMessages] = useState<MessageType[]>([])
const [lastMessageReceived, setLastMessageReceived] = useState<boolean>(
false
)
const [lastMessageReceived, setLastMessageReceived] =
useState<boolean>(false)

const loadMoreSpinnerRef = useRef<HTMLDivElement>(null)

const fetchMessages = useCallback(() => {
const after = messages[0]?.id
const url = `${apiUrl}/timeline` + (after ? `?after=${after}` : '')

fetch(url)
.then(response => response.json())
getTimeline({ after: after ? after : null })
.then(response => {
if (response.data?.messages) {
setMessages([...response.data?.messages, ...messages])
if (response.data.messages) {
setMessages([...response.data.messages, ...messages])
}
setIsLoading(false)
})
Expand All @@ -39,10 +36,9 @@ const MessageList: FC<Props> = props => {
const fetchOlderMessages = useCallback(() => {
const oldestMessage = messages[messages.length - 1]
if (oldestMessage !== undefined) {
fetch(`${apiUrl}/timeline?before=${oldestMessage.id}`)
.then(response => response.json())
getTimeline({ before: oldestMessage.id })
.then(response => {
if (response.data?.messages !== null) {
if (response.data.messages !== null) {
setMessages([...messages, ...response.data.messages])
} else {
setLastMessageReceived(true)
Expand Down
57 changes: 57 additions & 0 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Message, Settings, Ticker } from './types'

const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/v1'

type InitResponseData = {
settings: Settings
ticker: Ticker | null
}

export type InitResponse = {
data: InitResponseData
}

type TimelineResponseData = {
messages: Message[]
}

export type TimelineResponse = {
data: TimelineResponseData
}

async function get<T>(path: string, config?: RequestInit): Promise<T> {
const init = { method: 'get', ...config }
const request = new Request(path, init)
const response = await fetch(request)

if (!response.ok) {
throw new Error(
`The server responses with an error: ${response.statusText} (${response.status})`
)
}

return response.json().catch(() => ({}))
}

export async function getInit(): Promise<InitResponse> {
return get(`${apiUrl}/init`)
}

export type TimelineOpts = {
after?: string | null
before?: string | null
}

export async function getTimeline(
opts: TimelineOpts
): Promise<TimelineResponse> {
if (opts.after != null) {
return get(`${apiUrl}/timeline?after=${opts.after}`)
}

if (opts.before != null) {
return get(`${apiUrl}/timeline?before=${opts.before}`)
}

return get(`${apiUrl}/timeline`)
}
3 changes: 0 additions & 3 deletions src/lib/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ export const isMobile = (): boolean => {
return width < breakpoints.mobile
}

export const apiUrl =
process.env.REACT_APP_API_URL || 'http://localhost:8080/v1'

// FIXME: Might be better to use a library like validator.js
// to catch more cases.
export const replaceMagic = (text: string): string => {
Expand Down
5 changes: 5 additions & 0 deletions src/setupTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom'
Loading