Skip to content

Commit b80401c

Browse files
committed
Add test suite
- Auth redirect flow - Playlist loading (snapshot) - Single playlist export - Export all playlists
1 parent bd013f1 commit b80401c

File tree

12 files changed

+1134
-56
lines changed

12 files changed

+1134
-56
lines changed

.travis.yml

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,9 @@
1-
os: osx
2-
3-
before_install:
4-
- brew cask install phantomjs
5-
- git clone git://github.com/n1k0/casperjs.git /tmp/casperjs
6-
- export PATH=/tmp/casperjs/bin/:$PATH
7-
8-
before_script:
9-
- phantomjs --version
10-
- casperjs --version
11-
- python -m SimpleHTTPServer 8080 &
12-
- sleep 10
13-
1+
language: node_js
2+
node_js:
3+
- "stable"
4+
cache:
5+
directories:
6+
- node_modules
147
script:
15-
- casperjs test test/integration
16-
17-
deploy:
18-
provider: pages
19-
skip_cleanup: true
20-
github_token: $GITHUB_TOKEN
21-
keep_history: true
22-
on:
23-
branch: master
8+
- yarn build
9+
- yarn test

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,9 @@
5151
"last 1 firefox version",
5252
"last 1 safari version"
5353
]
54+
},
55+
"devDependencies": {
56+
"msw": "^0.21.3",
57+
"react-test-renderer": "^17.0.1"
5458
}
5559
}

src/App.test.jsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React from "react"
2+
import { render, screen, fireEvent } from "@testing-library/react"
3+
import { rest } from 'msw'
4+
import App from "./App"
5+
6+
const { location } = window
7+
8+
beforeAll(() => {
9+
delete window.location
10+
})
11+
12+
afterAll(() => {
13+
window.location = location
14+
})
15+
16+
describe("authentication request", () => {
17+
beforeAll(() => {
18+
window.location = { hash: "" }
19+
})
20+
21+
test("renders get started button and redirects to Spotify with correct scopes", () => {
22+
render(<App />)
23+
24+
const linkElement = screen.getByText(/Get Started/i)
25+
26+
expect(linkElement).toBeInTheDocument()
27+
28+
fireEvent.click(linkElement)
29+
30+
expect(window.location.href).toBe(
31+
"https://accounts.spotify.com/authorize?client_id=9950ac751e34487dbbe027c4fd7f8e99&redirect_uri=%2F%2F&scope=playlist-read-private%20playlist-read-collaborative%20user-library-read&response_type=token"
32+
)
33+
})
34+
})
35+
36+
describe("authentication return", () => {
37+
beforeAll(() => {
38+
window.location = { hash: "#access_token=TEST_TOKEN" }
39+
})
40+
41+
test("renders playlist component on return from Spotify with auth token", () => {
42+
render(<App />)
43+
44+
expect(screen.getByTestId('playlistTableSpinner')).toBeInTheDocument()
45+
})
46+
})

src/App.test.tsx

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/App.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,20 @@
11
import './App.scss'
2+
import "./icons"
23

34
import React from 'react'
45
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
5-
import { library } from '@fortawesome/fontawesome-svg-core'
6-
import { fab } from '@fortawesome/free-brands-svg-icons'
7-
import { faCheckCircle, faTimesCircle, faFileArchive, faHeart } from '@fortawesome/free-regular-svg-icons'
8-
import { faBolt, faMusic } from '@fortawesome/free-solid-svg-icons'
96

107
import Login from 'components/Login'
118
import PlaylistTable from "components/PlaylistTable"
129
import { getQueryParam } from "helpers"
1310

14-
library.add(fab, faCheckCircle, faTimesCircle, faFileArchive, faHeart, faBolt, faMusic)
15-
1611
function App() {
1712
let view
1813
let key = new URLSearchParams(window.location.hash.substring(1))
1914

2015
if (getQueryParam('rate_limit_message') !== '') {
2116
view = <div id="rateLimitMessage" className="lead">
22-
<p><FontAwesomeIcon icon={faBolt} style={{ fontSize: "50px", marginBottom: "20px" }} /></p>
17+
<p><FontAwesomeIcon icon={['fas', 'bolt']} style={{ fontSize: "50px", marginBottom: "20px" }} /></p>
2318
<p>Oops, Exportify has encountered a <a target="_blank" rel="noreferrer" href="https://developer.spotify.com/web-api/user-guide/#rate-limiting">rate limiting</a> error while using the Spotify API. This might be because of the number of users currently exporting playlists, or perhaps because you have too many playlists to export all at once. Try <a target="_blank" rel="noreferrer" href="https://github.com/watsonbox/exportify/issues/6#issuecomment-110793132">creating your own</a> Spotify application. If that doesn't work, please add a comment to <a target="_blank" rel="noreferrer" href="https://github.com/watsonbox/exportify/issues/6">this issue</a> where possible resolutions are being discussed.</p>
2419
<p style={{ marginTop: "50px" }}>It should still be possible to export individual playlists, particularly when using your own Spotify application.</p>
2520
</div>

src/components/PlaylistTable.jsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ class PlaylistTable extends React.Component {
1111
state = {
1212
playlists: [],
1313
playlistCount: 0,
14+
likedSongsLimit: 0,
15+
likedSongsCount: 0,
1416
nextURL: null,
1517
prevURL: null
1618
}
@@ -68,6 +70,12 @@ class PlaylistTable extends React.Component {
6870
},
6971
"uri": "spotify:user:" + userId + ":saved"
7072
});
73+
74+
// FIXME: Handle unmounting
75+
this.setState({
76+
likedSongsLimit: arguments[0][0].limit,
77+
likedSongsCount: arguments[0][0].total
78+
})
7179
}
7280

7381
// FIXME: Handle unmounting
@@ -85,7 +93,7 @@ class PlaylistTable extends React.Component {
8593
}
8694

8795
exportPlaylists() {
88-
PlaylistsExporter.export(this.props.access_token, this.state.playlistCount);
96+
PlaylistsExporter.export(this.props.access_token, this.state.playlistCount, this.state.likedSongsLimit, this.state.likedSongsCount);
8997
}
9098

9199
componentDidMount() {
@@ -123,7 +131,7 @@ class PlaylistTable extends React.Component {
123131
</div>
124132
);
125133
} else {
126-
return <div className="spinner"></div>
134+
return <div className="spinner" data-testid="playlistTableSpinner"></div>
127135
}
128136
}
129137
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import React from "react"
2+
import { render, screen, waitFor, fireEvent } from "@testing-library/react"
3+
import renderer from "react-test-renderer"
4+
import { setupServer } from "msw/node"
5+
import FileSaver from "file-saver"
6+
import JSZip from "jszip"
7+
8+
import PlaylistTable from "./PlaylistTable"
9+
10+
import "../icons"
11+
import { handlers } from '../mocks/handlers_success'
12+
13+
const server = setupServer(...handlers)
14+
15+
server.listen({
16+
onUnhandledRequest: 'warn'
17+
})
18+
19+
beforeAll(() => {
20+
global.Blob = function (content, options) { return ({content, options}) }
21+
})
22+
23+
afterEach(() => {
24+
jest.restoreAllMocks()
25+
})
26+
27+
// Use a snapshot test to ensure exact component rendering
28+
test("playlist loading", async () => {
29+
const component = renderer.create(<PlaylistTable />)
30+
const instance = component.getInstance()
31+
32+
await waitFor(() => {
33+
expect(instance.state.playlistCount).toEqual(1)
34+
})
35+
36+
expect(component.toJSON()).toMatchSnapshot();
37+
})
38+
39+
test("single playlist exporting", async () => {
40+
const saveAsMock = jest.spyOn(FileSaver, "saveAs")
41+
saveAsMock.mockImplementation(jest.fn())
42+
43+
render(<PlaylistTable />);
44+
45+
await waitFor(() => {
46+
expect(screen.getByText(/Export All/)).toBeInTheDocument()
47+
})
48+
49+
const linkElement = screen.getAllByText("Export")[0]
50+
51+
expect(linkElement).toBeInTheDocument()
52+
53+
fireEvent.click(linkElement)
54+
55+
await waitFor(() => {
56+
expect(saveAsMock).toHaveBeenCalledTimes(1)
57+
expect(saveAsMock).toHaveBeenCalledWith(
58+
{
59+
content: [
60+
'"Track URI","Track Name","Artist URI","Artist Name","Album URI","Album Name","Disc Number","Track Number","Track Duration (ms)","Added By","Added At"\n' +
61+
'"spotify:track:1GrLfs4TEvAZ86HVzXHchS","Crying","spotify:artist:4TXdHyuAOl3rAOFmZ6MeKz","Six by Seven","spotify:album:4iwv7b8gDPKztLkKCbWyhi","Best of Six By Seven","1","3","198093","","2020-07-19T09:24:39Z"\n'
62+
],
63+
options: { type: 'text/csv;charset=utf-8' }
64+
},
65+
'liked.csv',
66+
true
67+
)
68+
})
69+
})
70+
71+
test("exporting of all playlist", async () => {
72+
const saveAsMock = jest.spyOn(FileSaver, "saveAs")
73+
saveAsMock.mockImplementation(jest.fn())
74+
75+
const jsZipFileMock = jest.spyOn(JSZip.prototype, 'file')
76+
const jsZipGenerateAsync = jest.spyOn(JSZip.prototype, 'generateAsync')
77+
jsZipGenerateAsync.mockResolvedValue("zip_content")
78+
79+
render(<PlaylistTable />);
80+
81+
await waitFor(() => {
82+
expect(screen.getByText(/Export All/)).toBeInTheDocument()
83+
})
84+
85+
const linkElement = screen.getByText("Export All")
86+
87+
expect(linkElement).toBeInTheDocument()
88+
89+
fireEvent.click(linkElement)
90+
91+
await waitFor(() => {
92+
expect(jsZipFileMock).toHaveBeenCalledTimes(2)
93+
expect(jsZipFileMock).toHaveBeenCalledWith(
94+
"liked.csv",
95+
'"Track URI","Track Name","Artist URI","Artist Name","Album URI","Album Name","Disc Number","Track Number","Track Duration (ms)","Added By","Added At"\n' +
96+
'"spotify:track:1GrLfs4TEvAZ86HVzXHchS","Crying","spotify:artist:4TXdHyuAOl3rAOFmZ6MeKz","Six by Seven","spotify:album:4iwv7b8gDPKztLkKCbWyhi","Best of Six By Seven","1","3","198093","","2020-07-19T09:24:39Z"\n'
97+
)
98+
expect(jsZipFileMock).toHaveBeenCalledWith(
99+
"ghostpoet_–_peanut_butter_blues_and_melancholy_jam.csv",
100+
'"Track URI","Track Name","Artist URI","Artist Name","Album URI","Album Name","Disc Number","Track Number","Track Duration (ms)\",\"Added By\",\"Added At\"\n' +
101+
'"spotify:track:7ATyvp3TmYBmGW7YuC8DJ3","One Twos / Run Run Run","spotify:artist:69lEbRQRe29JdyLrewNAvD","Ghostpoet","spotify:album:6jiLkuSnhzDvzsHJlweoGh","Peanut Butter Blues and Melancholy Jam","1","1","241346","spotify:user:watsonbox","2020-11-03T15:19:04Z"\n' +
102+
'"spotify:track:0FNanBLvmFEDyD75Whjj52","Us Against Whatever Ever","spotify:artist:69lEbRQRe29JdyLrewNAvD","Ghostpoet","spotify:album:6jiLkuSnhzDvzsHJlweoGh","Peanut Butter Blues and Melancholy Jam","1","2","269346","spotify:user:watsonbox","2020-11-03T15:19:04Z"\n'
103+
)
104+
})
105+
106+
await waitFor(() => {
107+
expect(saveAsMock).toHaveBeenCalledTimes(1)
108+
expect(saveAsMock).toHaveBeenCalledWith("zip_content", "spotify_playlists.zip")
109+
})
110+
})

src/components/PlaylistsExporter.jsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import $ from "jquery" // TODO: Remove jQuery dependency
22
import { saveAs } from "file-saver"
3-
import { JSZip } from "jszip"
3+
import JSZip from "jszip"
44

55
import PlaylistExporter from "./PlaylistExporter"
66
import { apiCall } from "helpers"
77

88
// Handles exporting all playlist data as a zip file
99
let PlaylistsExporter = {
10-
export: function(access_token, playlistCount) {
10+
export: function(access_token, playlistCount, likedSongsLimit, likedSongsCount) {
1111
var playlistFileNames = [];
1212

1313
apiCall("https://api.spotify.com/v1/me", access_token).then(function(response) {
@@ -44,12 +44,12 @@ let PlaylistsExporter = {
4444

4545
// Add library of saved tracks
4646
playlists.unshift({
47-
"id": "saved",
48-
"name": "Saved",
47+
"id": "liked",
48+
"name": "Liked",
4949
"tracks": {
5050
"href": "https://api.spotify.com/v1/me/tracks",
51-
"limit": 50,
52-
"total": 2500 // TODO: get rid of hard-coded library size
51+
"limit": likedSongsLimit,
52+
"total": likedSongsCount
5353
},
5454
});
5555

@@ -66,8 +66,9 @@ let PlaylistsExporter = {
6666
zip.file(playlistFileNames[i], response)
6767
});
6868

69-
var content = zip.generate({ type: "blob" });
70-
saveAs(content, "spotify_playlists.zip");
69+
zip.generateAsync({ type: "blob" }).then(function(content) {
70+
saveAs(content, "spotify_playlists.zip");
71+
})
7172
});
7273
});
7374
}

0 commit comments

Comments
 (0)