Compare commits

..

10 Commits

Author SHA1 Message Date
Colin Diem
ac15c86840 Chapter 6 - Adding Comments to Schema - Complete 2022-04-26 01:24:39 -04:00
e180c93ada Chapter 6 - Building a Component the Redwood Way - Testing 2022-04-26 00:01:11 -04:00
5d9a23422b Chapter 5 - Testing Article 2022-04-25 23:27:47 -04:00
renovate[bot]
1c2d6f2b4a Update redwood monorepo to v1.1.1 (#43)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-04-22 09:02:51 -07:00
hello there
03436e2156 Update .gitignore to match create-redwood-app template (#42) 2022-04-21 18:52:45 -07:00
renovate[bot]
9eb54f11cb Update redwood monorepo to v1.1.0 (#41)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-04-19 22:27:28 -07:00
renovate[bot]
24ec0c3188 Update redwood monorepo to v1.0.2 (#40)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-04-15 13:44:36 -07:00
renovate[bot]
7d27023ef6 Update redwood monorepo to v1.0.1 (#38) 2022-04-14 06:27:28 -07:00
Rob Cameron
4e7012899c Fix label names 2022-04-04 18:53:36 -07:00
renovate[bot]
43f0b7ebad Update redwood monorepo to v1 (#34)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-04-04 07:20:39 -07:00
24 changed files with 1763 additions and 988 deletions

10
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.idea
.DS_Store
.env
.netlify
@@ -7,8 +8,13 @@ dist
dist-babel
node_modules
yarn-error.log
redwood/*
web/public/mockServiceWorker.js
web/types/graphql.d.ts
api/types/graphql.d.ts
web/public/mockServiceWorker.js
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

View File

@@ -0,0 +1,9 @@
-- CreateTable
CREATE TABLE "Comment" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"body" TEXT NOT NULL,
"postId" INTEGER NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Comment_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);

View File

@@ -12,6 +12,7 @@ model Post {
id Int @id @default(autoincrement())
title String
body String
comments Comment[]
createdAt DateTime @default(now())
}
@@ -32,3 +33,12 @@ model User {
resetToken String?
resetTokenExpiresAt DateTime?
}
model Comment {
id Int @id @default(autoincrement())
name String
body String
post Post @relation(fields: [postId], references: [id])
postId Int
createdAt DateTime @default(now())
}

View File

@@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@redwoodjs/api": "^0.50.0",
"@redwoodjs/graphql-server": "^0.50.0"
"@redwoodjs/api": "1.1.1",
"@redwoodjs/graphql-server": "1.1.1"
}
}

View File

@@ -0,0 +1,31 @@
export const schema = gql`
type Comment {
id: Int!
name: String!
body: String!
post: Post!
postId: Int!
createdAt: DateTime!
}
type Query {
comments: [Comment!]! @skipAuth
}
input CreateCommentInput {
name: String!
body: String!
postId: Int!
}
input UpdateCommentInput {
name: String
body: String
postId: Int
}
type Mutation {
createComment(input: CreateCommentInput!): Comment! @skipAuth
deleteComment(id: Int!): Comment! @requireAuth
}
`

View File

@@ -0,0 +1,28 @@
import { db } from 'src/lib/db'
export const comments = () => {
return db.comment.findMany()
}
export const comment = ({ id }) => {
return db.comment.findUnique({
where: { id },
})
}
export const Comment = {
post: (_obj, { root }) =>
db.comment.findUnique({ where: { id: root.id } }).post(),
}
export const createComment = ({ input }) => {
return db.comment.create({
data: input,
})
}
export const deleteComment = ({ id }) => {
return db.comment.delete({
where: { id },
})
}

View File

@@ -0,0 +1,42 @@
import { defineScenario } from '@redwoodjs/testing/dist/api'
export const standard = defineScenario({
comment: {
one: {
data: {
name: 'Jane Doe',
body: 'I like trees.',
post: {
create: {
title: 'Redwood Leaves',
body: 'The quick brown fox jumped over the lazy dog.',
},
},
},
},
two: {
data: {
name: 'John Doe',
body: 'Hug a tree today',
post: {
create: {
title: 'Root Systems',
body: 'The five boxing wizards jump quickly',
},
},
},
},
},
})
export const postOnly = defineScenario({
post: {
bark: {
data: {
title: 'Bark',
body: "A tree's bark is worse than its bite",
},
},
},
})

View File

@@ -0,0 +1,30 @@
import { comments, createComment } from './comments'
// Generated boilerplate tests do not account for all circumstances
// and can fail without adjustments, e.g. Float and DateTime types.
// Please refer to the RedwoodJS Testing Docs:
// https://redwoodjs.com/docs/testing#testing-services
// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
describe('comments', () => {
scenario('returns all comments', async (scenario) => {
const result = await comments()
expect(result.length).toEqual(Object.keys(scenario.comment).length)
})
scenario('postOnly', 'creates a new comment', async (scenario) => {
const comment = await createComment({
input: {
name: 'Billy Bob',
body: 'What is your favorite tree bark?',
postId: scenario.post.bark.id,
},
})
expect(comment.name).toEqual('Billy Bob')
expect(comment.body).toEqual('What is your favorite tree bark?')
expect(comment.postId).toEqual(scenario.post.bark.id)
expect(comment.createdAt).not.toEqual(null)
})
})

View File

@@ -7,7 +7,7 @@
]
},
"devDependencies": {
"@redwoodjs/core": "^0.50.0"
"@redwoodjs/core": "1.1.1"
},
"eslintConfig": {
"extends": "@redwoodjs/eslint-config"

View File

@@ -13,10 +13,10 @@
]
},
"dependencies": {
"@redwoodjs/auth": "^0.50.0",
"@redwoodjs/forms": "^0.50.0",
"@redwoodjs/router": "^0.50.0",
"@redwoodjs/web": "^0.50.0",
"@redwoodjs/auth": "1.1.1",
"@redwoodjs/forms": "1.1.1",
"@redwoodjs/router": "1.1.1",
"@redwoodjs/web": "1.1.1",
"prop-types": "15.8.1",
"react": "17.0.2",
"react-dom": "17.0.2"

View File

@@ -1,6 +1,11 @@
import { Link, routes } from '@redwoodjs/router'
import CommentsCell from 'src/components/CommentsCell'
const Article = ({ article }) => {
const truncate = (text, length) => {
return text.substring(0, length) + '...'
}
const Article = ({ article, summary = false }) => {
return (
<article>
<header>
@@ -8,7 +13,14 @@ const Article = ({ article }) => {
<Link to={routes.article({ id: article.id })}>{article.title}</Link>
</h2>
</header>
<div className="mt-2 text-gray-900 font-light">{article.body}</div>
<div className="mt-2 text-gray-900 font-light">
{summary ? truncate(article.body, 100) : article.body}
</div>
{!summary && (
<div className="mt-12">
<CommentsCell />
</div>
)}
</article>
)
}

View File

@@ -1,17 +1,17 @@
import Article from './Article'
export const generated = () => {
return (
<Article
article={{
const ARTICLE = {
id: 1,
title: 'First Post',
body:
'Neutra tacos hot chicken prism raw denim, put a bird on it enamel pin post-ironic vape cred DIY. Street art next level umami squid. Hammock hexagon glossier 8-bit banjo. Neutra la croix mixtape echo park four loko semiotics kitsch forage chambray. Semiotics salvia selfies jianbing hella shaman. Letterpress helvetica vaporware cronut, shaman butcher YOLO poke fixie hoodie gentrify woke heirloom.',
createdAt: '2020-01-01T12:34:56Z',
}}
/>
)
body: 'Neutra tacos hot chicken prism raw denim, put a bird on it enamel pin post-ironic vape cred DIY. Street art next level umami squid. Hammock hexagon glossier 8-bit banjo. Neutra la croix mixtape echo park four loko semiotics kitsch forage chambray. Semiotics salvia selfies jianbing hella shaman. Letterpress helvetica vaporware cronut, shaman butcher YOLO poke fixie hoodie gentrify woke heirloom.',
}
export const full = () => {
return <Article article={ARTICLE} />
}
export const summary = () => {
return <Article article={ARTICLE} summary={true} />
}
export default { title: 'Components/Article' }

View File

@@ -1,18 +1,49 @@
import { render, screen } from '@redwoodjs/testing'
import { render, screen, waitFor } from '@redwoodjs/testing'
import { standard } from 'src/components/CommentsCell/CommentsCell.mock'
import Article from './Article'
describe('Article', () => {
it('renders a blog post', () => {
const article = {
const ARTICLE = {
id: 1,
title: 'First post',
body: `Neutra tacos hot chicken prism raw denim, put a bird on it enamel pin post-ironic vape cred DIY. Street art next level umami squid. Hammock hexagon glossier 8-bit banjo. Neutra la croix mixtape echo park four loko semiotics kitsch forage chambray. Semiotics salvia selfies jianbing hella shaman. Letterpress helvetica vaporware cronut, shaman butcher YOLO poke fixie hoodie gentrify woke heirloom.`,
createdAt: new Date().toISOString(),
}
render(<Article article={article} />)
expect(screen.getByText(article.title)).toBeInTheDocument()
expect(screen.getByText(article.body)).toBeInTheDocument()
describe('Article', () => {
it('renders a blog post', () => {
render(<Article article={ARTICLE} />)
expect(screen.getByText(ARTICLE.title)).toBeInTheDocument()
expect(screen.getByText(ARTICLE.body)).toBeInTheDocument()
})
it('renders comments when displaying a full blog post', async () => {
const comment = standard().comments[0]
render(<Article article={ARTICLE} />)
await waitFor(() =>
expect(screen.getByText(comment.body)).toBeInTheDocument()
)
})
it('renders a summary of a blog post', () => {
render(<Article article={ARTICLE} summary={true} />)
expect(screen.getByText(ARTICLE.title)).toBeInTheDocument()
expect(
screen.getByText(
'Neutra tacos hot chicken prism raw denim, put a bird on it enamel pin post-ironic vape cred DIY. Str...'
)
).toBeInTheDocument()
})
it('does not render comments when displaying a summary', async () => {
const comment = standard().comments[0]
render(<Article article={ARTICLE} summary={true} />)
await waitFor(() =>
expect(screen.queryByText(comment.body)).not.toBeInTheDocument()
)
})
})

View File

@@ -21,7 +21,7 @@ export const Success = ({ articles }) => {
return (
<div className="space-y-10">
{articles.map((article) => (
<Article article={article} key={article.id} />
<Article article={article} key={article.id} summary={true} />
))}
</div>
)

View File

@@ -1,4 +1,4 @@
import { render, screen } from '@redwoodjs/testing'
import { render, screen, within } from '@redwoodjs/testing'
import { Loading, Empty, Failure, Success } from './ArticlesCell'
import { standard } from './ArticlesCell.mock'
@@ -25,9 +25,15 @@ describe('ArticlesCell', () => {
const articles = standard().articles
render(<Success articles={articles} />)
expect(screen.getByText(articles[0].title)).toBeInTheDocument()
expect(screen.getByText(articles[0].body)).toBeInTheDocument()
expect(screen.getByText(articles[1].title)).toBeInTheDocument()
expect(screen.getByText(articles[1].body)).toBeInTheDocument()
articles.forEach((article) => {
const truncatedBody = article.body.substring(0, 10)
const matchedBody = screen.getByText(truncatedBody, { exact: false })
const ellipsis = within(matchedBody).getByText('...', { exact: false })
expect(screen.getByText(article.title)).toBeInTheDocument()
expect(screen.queryByText(article.body)).not.toBeInTheDocument()
expect(matchedBody).toBeInTheDocument()
expect(ellipsis).toBeInTheDocument()
})
})
})

View File

@@ -0,0 +1,21 @@
const formattedDate = (datetime) => {
const parsedDate = new Date(datetime)
const month = parsedDate.toLocaleString('default', { month: 'long' })
return `${parsedDate.getDate()} ${month} ${parsedDate.getFullYear()}`
}
const Comment = ({ comment }) => {
return (
<div className="bg-gray-200 p-8 rounded-lg">
<header className="flex justify-between">
<h2 className="font-semibold text-gray-700">{comment.name}</h2>
<time className="text-xs text-gray-500" dateTime={comment.createdAt}>
{formattedDate(comment.createdAt)}
</time>
</header>
<p className="text-sm mt-2">{comment.body}</p>
</div>
)
}
export default Comment

View File

@@ -0,0 +1,15 @@
import Comment from './Comment'
export const generated = () => {
return (
<Comment
comment={{
name: 'Rob Cameron',
body: 'This is the first comment!',
createdAt: '2022-04-25T23:53:34Z',
}}
/>
)
}
export default { title: 'Components/Comment' }

View File

@@ -0,0 +1,23 @@
import { render, screen } from '@redwoodjs/testing/web'
import Comment from './Comment'
// Improve this test with help from the Redwood Testing Doc:
// https://redwoodjs.com/docs/testing#testing-components
describe('Comment', () => {
it('renders successfully', () => {
const comment = {
name: 'John Doe',
body: 'This is my comment',
createdAt: '2020-01-02T12:34:56Z',
}
render(<Comment comment={comment} />)
expect(screen.getByText(comment.name)).toBeInTheDocument()
expect(screen.getByText(comment.body)).toBeInTheDocument()
const dateExpect = screen.getByText('2 January 2020')
expect(dateExpect).toBeInTheDocument()
expect(dateExpect.nodeName).toEqual('TIME')
expect(dateExpect).toHaveAttribute('datetime', comment.createdAt)
})
})

View File

@@ -0,0 +1,32 @@
import Comment from 'src/components/Comment'
export const QUERY = gql`
query CommentsQuery {
comments {
id
name
body
createdAt
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => (
<div className="text-center text-gray-500">No comments yet</div>
)
export const Failure = ({ error }) => (
<div style={{ color: 'red' }}>Error: {error.message}</div>
)
export const Success = ({ comments }) => {
return (
<div className="space-y-8">
{comments.map((comment) => (
// eslint-disable-next-line prettier/prettier
<Comment key={comment.id} comment={comment} />
))}
</div>
)
}

View File

@@ -0,0 +1,17 @@
// Define your own mock data here:
export const standard = () => ({
comments: [
{
id: 1,
name: 'Rob Cameron',
body: 'First comment',
createdAt: '2020-01-02T12:34:56Z',
},
{
id: 2,
name: 'David Price',
body: 'Second comment',
createdAt: '2020-02-03T23:00:00Z',
},
],
})

View File

@@ -0,0 +1,20 @@
import { Loading, Empty, Failure, Success } from './CommentsCell'
import { standard } from './CommentsCell.mock'
export const loading = () => {
return Loading ? <Loading /> : null
}
export const empty = () => {
return Empty ? <Empty /> : null
}
export const failure = () => {
return Failure ? <Failure error={new Error('Oh no')} /> : null
}
export const success = () => {
return Success ? <Success {...standard()} /> : null
}
export default { title: 'Cells/CommentsCell' }

View File

@@ -0,0 +1,43 @@
import { render, screen } from '@redwoodjs/testing/web'
import { Loading, Empty, Failure, Success } from './CommentsCell'
import { standard } from './CommentsCell.mock'
// Generated boilerplate tests do not account for all circumstances
// and can fail without adjustments, e.g. Float and DateTime types.
// Please refer to the RedwoodJS Testing Docs:
// https://redwoodjs.com/docs/testing#testing-cells
// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
describe('CommentsCell', () => {
it('renders Loading successfully', () => {
expect(() => {
render(<Loading />)
}).not.toThrow()
})
it('renders Empty successfully', async () => {
render(<Empty />)
expect(screen.getByText('No comments yet')).toBeInTheDocument()
})
it('renders Failure successfully', async () => {
expect(() => {
render(<Failure error={new Error('Oh no')} />)
}).not.toThrow()
})
// When you're ready to test the actual output of your component render
// you could test that, for example, certain text is present:
//
// 1. import { screen } from '@redwoodjs/testing/web'
// 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument()
it('renders Success successfully', async () => {
const comments = standard().comments
render(<Success comments={comments} />)
comments.forEach((comment) => {
expect(screen.getByText(comment.body)).toBeInTheDocument()
})
})
})

View File

@@ -68,7 +68,7 @@ const ContactPage = () => {
<FieldError name="name" className="block text-red-700" />
<Label
name="name"
name="email"
className="block mt-8 text-gray-700 uppercase text-sm"
errorClassName="block mt-8 text-red-700 uppercase text-sm"
>
@@ -89,7 +89,7 @@ const ContactPage = () => {
<FieldError name="email" className="block text-red-700" />
<Label
name="name"
name="message"
className="block mt-8 text-gray-700 uppercase text-sm"
errorClassName="block mt-8 text-red-700 uppercase text-sm"
>

2285
yarn.lock

File diff suppressed because it is too large Load Diff