@stone-js/starters 0.8.3 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/basic-react-declarative/app/Application.tsx +1 -0
- package/basic-react-declarative/package.json +5 -5
- package/basic-react-declarative/public/logo.svg +16 -0
- package/basic-react-declarative/tests/Application.spec.ts +11 -2
- package/basic-react-imperative/app/Application.tsx +1 -0
- package/basic-react-imperative/package.json +5 -5
- package/basic-react-imperative/public/logo.svg +16 -0
- package/basic-react-imperative/tests/Application.spec.ts +11 -2
- package/basic-service-declarative/package.json +4 -4
- package/basic-service-imperative/package.json +4 -4
- package/continuum-showcase/LICENSE +21 -0
- package/continuum-showcase/app/Application.ts +45 -0
- package/continuum-showcase/app/layouts/MainLayout.tsx +1 -2
- package/continuum-showcase/app/pages/BlogPostPage.tsx +1 -1
- package/continuum-showcase/app/pages/HomePage.tsx +2 -2
- package/continuum-showcase/assets/css/index.css +26 -0
- package/continuum-showcase/assets/images/logo.svg +16 -0
- package/continuum-showcase/package.json +22 -10
- package/continuum-showcase/tests/HomePage.spec.tsx +40 -0
- package/continuum-showcase/tsconfig.json +33 -0
- package/continuum-showcase/vitest.config.ts +31 -0
- package/full-react-declarative/app/layout/AppLayout/AppLayout.tsx +1 -1
- package/full-react-declarative/app/layout/ErrorLayout/ErrorLayout.tsx +1 -1
- package/full-react-declarative/app/layout/SecurityLayout/SecurityLayout.tsx +1 -1
- package/full-react-declarative/package.json +10 -10
- package/full-react-declarative/public/logo.svg +16 -0
- package/full-react-declarative/tests/AppLayout.spec.ts +51 -0
- package/full-react-imperative/app/components/LayoutHeader/LayoutHeader.tsx +1 -1
- package/full-react-imperative/app/components/PostWidget/PostWidget.tsx +1 -1
- package/full-react-imperative/app/components/PullToRefresh/PullToRefresh.css +10 -0
- package/full-react-imperative/app/components/PullToRefresh/PullToRefresh.tsx +66 -0
- package/full-react-imperative/app/layout/ErrorLayout/ErrorLayout.tsx +1 -1
- package/full-react-imperative/app/layout/SecurityLayout/SecurityLayout.tsx +1 -1
- package/full-react-imperative/package.json +11 -12
- package/full-react-imperative/public/logo.svg +16 -0
- package/full-react-imperative/tests/LayoutHeader.spec.ts +39 -0
- package/full-service-declarative/package.json +9 -9
- package/full-service-declarative/tests/PostService.spec.ts +56 -0
- package/full-service-declarative/vitest.config.ts +1 -1
- package/full-service-imperative/package.json +9 -9
- package/full-service-imperative/tests/PostService.spec.ts +47 -0
- package/full-service-imperative/vitest.config.ts +1 -1
- package/package.json +1 -1
- package/standard-react-declarative/app/pages/welcome/WelcomePage.tsx +1 -0
- package/standard-react-declarative/package.json +8 -8
- package/standard-react-declarative/public/logo.svg +16 -0
- package/standard-react-declarative/tests/WelcomePage.spec.ts +69 -0
- package/standard-react-imperative/app/pages/welcome/WelcomePage.tsx +1 -0
- package/standard-react-imperative/package.json +8 -8
- package/standard-react-imperative/public/logo.svg +16 -0
- package/standard-react-imperative/tests/WelcomePage.spec.ts +51 -0
- package/standard-service-declarative/package.json +6 -6
- package/standard-service-declarative/tests/Application.spec.ts +91 -0
- package/standard-service-declarative/vitest.config.ts +1 -1
- package/standard-service-imperative/package.json +6 -6
- package/standard-service-imperative/tests/Application.spec.ts +75 -0
- package/standard-service-imperative/vitest.config.ts +1 -1
- package/basic-react-declarative/public/logo.png +0 -0
- package/basic-react-declarative/tests/__snapshots__/Application.spec.ts.snap +0 -3
- package/basic-react-imperative/public/logo.png +0 -0
- package/basic-react-imperative/tests/__snapshots__/Application.spec.ts.snap +0 -3
- package/full-react-declarative/assets/img/logo.png +0 -0
- package/full-react-declarative/public/logo.png +0 -0
- package/full-react-imperative/assets/img/logo.png +0 -0
- package/full-react-imperative/public/logo.png +0 -0
|
@@ -2,9 +2,9 @@ import { User } from '../../models/User'
|
|
|
2
2
|
import { Post } from '../../models/Post'
|
|
3
3
|
import { deletePost } from '../../pages/utils'
|
|
4
4
|
import { PostCard } from '../PostCard/PostCard'
|
|
5
|
-
import PullToRefresh from 'react-pull-to-refresh'
|
|
6
5
|
import { IContainer, ILogger } from '@stone-js/core'
|
|
7
6
|
import { FC, useEffect, useRef, useState } from 'react'
|
|
7
|
+
import { PullToRefresh } from '../PullToRefresh/PullToRefresh'
|
|
8
8
|
import { PostSkeleton } from '../PostSkeleton/PostSkeleton'
|
|
9
9
|
import { CommentWidget } from '../CommentWidget/CommentWidget'
|
|
10
10
|
import { IRouter, ReactIncomingEvent } from '@stone-js/use-react'
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import './PullToRefresh.css'
|
|
2
|
+
import { FC, ReactNode, TouchEvent, useRef, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PullToRefresh options.
|
|
6
|
+
*/
|
|
7
|
+
export interface PullToRefreshOptions {
|
|
8
|
+
children: ReactNode
|
|
9
|
+
threshold?: number
|
|
10
|
+
onRefresh: () => Promise<void> | void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Lightweight, SSR-safe pull-to-refresh wrapper.
|
|
15
|
+
*
|
|
16
|
+
* Implemented in-app (with no dependency that touches `window` at import time)
|
|
17
|
+
* so it renders safely during server-side rendering and static route
|
|
18
|
+
* generation. The pull gesture is a progressive enhancement that only activates
|
|
19
|
+
* from touch interactions on the client.
|
|
20
|
+
*/
|
|
21
|
+
export const PullToRefresh: FC<PullToRefreshOptions> = ({ children, onRefresh, threshold = 64 }) => {
|
|
22
|
+
const startY = useRef<number | null>(null)
|
|
23
|
+
const [pullY, setPullY] = useState(0)
|
|
24
|
+
const [refreshing, setRefreshing] = useState(false)
|
|
25
|
+
|
|
26
|
+
const onTouchStart = (event: TouchEvent): void => {
|
|
27
|
+
if (refreshing) { return }
|
|
28
|
+
// Only start a pull when the page is scrolled to the very top.
|
|
29
|
+
if (document.documentElement.scrollTop <= 0) {
|
|
30
|
+
startY.current = event.touches[0].clientY
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const onTouchMove = (event: TouchEvent): void => {
|
|
35
|
+
if (startY.current === null || refreshing) { return }
|
|
36
|
+
const delta = event.touches[0].clientY - startY.current
|
|
37
|
+
if (delta > 0) { setPullY(Math.min(delta, threshold * 1.5)) }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const onTouchEnd = (): void => {
|
|
41
|
+
if (startY.current === null) { return }
|
|
42
|
+
const shouldRefresh = pullY >= threshold
|
|
43
|
+
startY.current = null
|
|
44
|
+
setPullY(0)
|
|
45
|
+
if (shouldRefresh) {
|
|
46
|
+
setRefreshing(true)
|
|
47
|
+
void Promise.resolve(onRefresh()).finally(() => setRefreshing(false))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
className='pull-to-refresh'
|
|
54
|
+
onTouchStart={onTouchStart}
|
|
55
|
+
onTouchMove={onTouchMove}
|
|
56
|
+
onTouchEnd={onTouchEnd}
|
|
57
|
+
style={{
|
|
58
|
+
transform: pullY > 0 ? `translateY(${pullY}px)` : undefined,
|
|
59
|
+
transition: pullY > 0 ? 'none' : 'transform .2s ease'
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
{refreshing && <div className='pull-to-refresh__indicator'>Refreshing…</div>}
|
|
63
|
+
{children}
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -18,7 +18,7 @@ export const ErrorLayout = (): IPageLayout => ({
|
|
|
18
18
|
<header className='mt-64'>
|
|
19
19
|
<div className='container'>
|
|
20
20
|
<p className='pa-24'>
|
|
21
|
-
<img src='/logo.
|
|
21
|
+
<img src='/logo.svg' className='logo' alt='Stone.js' />
|
|
22
22
|
</p>
|
|
23
23
|
</div>
|
|
24
24
|
</header>
|
|
@@ -18,7 +18,7 @@ export const SecurityLayout = (): IPageLayout => ({
|
|
|
18
18
|
<header className='mt-64'>
|
|
19
19
|
<div className='container'>
|
|
20
20
|
<p className='pa-24'>
|
|
21
|
-
<img src='/logo.
|
|
21
|
+
<img src='/logo.svg' className='logo' alt='Stone.js' />
|
|
22
22
|
</p>
|
|
23
23
|
</div>
|
|
24
24
|
</header>
|
|
@@ -18,26 +18,25 @@
|
|
|
18
18
|
"test:cvg": "npm run test -- --coverage"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@stone-js/env": "^0.8.
|
|
22
|
-
"@stone-js/http-core": "^0.8.
|
|
23
|
-
"@stone-js/node-http-adapter": "^0.8.
|
|
24
|
-
"@stone-js/router": "^0.8.
|
|
25
|
-
"@stone-js/filesystem": "^0.8.
|
|
26
|
-
"@stone-js/core": "^0.8.
|
|
27
|
-
"@stone-js/use-react": "^0.8.
|
|
28
|
-
"@stone-js/browser-adapter": "^0.8.
|
|
29
|
-
"@stone-js/node-cli-adapter": "^0.8.
|
|
21
|
+
"@stone-js/env": "^0.8.4",
|
|
22
|
+
"@stone-js/http-core": "^0.8.4",
|
|
23
|
+
"@stone-js/node-http-adapter": "^0.8.4",
|
|
24
|
+
"@stone-js/router": "^0.8.4",
|
|
25
|
+
"@stone-js/filesystem": "^0.8.4",
|
|
26
|
+
"@stone-js/core": "^0.8.4",
|
|
27
|
+
"@stone-js/use-react": "^0.8.4",
|
|
28
|
+
"@stone-js/browser-adapter": "^0.8.4",
|
|
29
|
+
"@stone-js/node-cli-adapter": "^0.8.4",
|
|
30
30
|
"axios": "^1.7.9",
|
|
31
31
|
"dayjs": "^1.11.13",
|
|
32
32
|
"react": "^19.0.0",
|
|
33
|
-
"react-dom": "^19.0.0"
|
|
34
|
-
"react-pull-to-refresh": "^2.0.1"
|
|
33
|
+
"react-dom": "^19.0.0"
|
|
35
34
|
},
|
|
36
35
|
"devDependencies": {
|
|
37
36
|
"@babel/plugin-proposal-decorators": "^7.25.9",
|
|
38
37
|
"@babel/preset-env": "^7.26.9",
|
|
39
38
|
"@babel/preset-typescript": "^7.27.0",
|
|
40
|
-
"@stone-js/cli": "^0.8.
|
|
39
|
+
"@stone-js/cli": "^0.8.4",
|
|
41
40
|
"@types/axios": "^0.14.4",
|
|
42
41
|
"@types/node": "^24.0.7",
|
|
43
42
|
"@types/react": "^19.0.7",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" width="96" height="96" role="img" aria-label="Stone.js">
|
|
2
|
+
<title>Stone.js</title>
|
|
3
|
+
<style>
|
|
4
|
+
.ink { stroke: #1B1613; }
|
|
5
|
+
@media (prefers-color-scheme: dark) { .ink { stroke: #EFE8DC; } }
|
|
6
|
+
</style>
|
|
7
|
+
<defs>
|
|
8
|
+
<linearGradient id="braise" gradientUnits="userSpaceOnUse" x1="22" y1="14" x2="74" y2="26.1">
|
|
9
|
+
<stop offset="0" stop-color="#FFC46B" />
|
|
10
|
+
<stop offset="1" stop-color="#FF5A1F" />
|
|
11
|
+
</linearGradient>
|
|
12
|
+
</defs>
|
|
13
|
+
<path class="ink" d="M 79.96 36.4 A 34 34 0 0 1 53.9 81.5" fill="none" stroke-width="11" stroke-linecap="round" opacity=".85" />
|
|
14
|
+
<path class="ink" d="M 42.1 81.5 A 34 34 0 0 1 16.05 36.4" fill="none" stroke-width="11" stroke-linecap="round" opacity=".85" />
|
|
15
|
+
<path d="M 22 26.1 A 34 34 0 0 1 74 26.1" fill="none" stroke="url(#braise)" stroke-width="11" stroke-linecap="round" />
|
|
16
|
+
</svg>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createElement } from 'react'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { LayoutHeader } from '../app/components/LayoutHeader/LayoutHeader'
|
|
4
|
+
|
|
5
|
+
// We must mock router-bound components to lighten the test environment
|
|
6
|
+
vi.mock('@stone-js/use-react', async (importOriginal) => {
|
|
7
|
+
const actual: any = await importOriginal()
|
|
8
|
+
return {
|
|
9
|
+
...actual,
|
|
10
|
+
StoneLink: ({ children }: any) => children
|
|
11
|
+
}
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe('LayoutHeader', () => {
|
|
15
|
+
const render = (): string => {
|
|
16
|
+
const container = {
|
|
17
|
+
make: (key: string) => key === 'event' ? { getUser: () => undefined } : {}
|
|
18
|
+
} as any
|
|
19
|
+
return renderToString(createElement(LayoutHeader, { container }))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
it('should render the real Stone.js logo', () => {
|
|
23
|
+
// Act
|
|
24
|
+
const html = render()
|
|
25
|
+
|
|
26
|
+
// Assert
|
|
27
|
+
expect(html).toContain('/logo.svg')
|
|
28
|
+
expect(html).toContain('alt="Stone.js"')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should render the Stone.js brand for an unauthenticated visitor', () => {
|
|
32
|
+
// Act
|
|
33
|
+
const html = render()
|
|
34
|
+
|
|
35
|
+
// Assert
|
|
36
|
+
expect(html).toContain('Stone.js')
|
|
37
|
+
expect(html).toContain('app-navbar')
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@libsql/client": "^0.14.0",
|
|
25
|
-
"@stone-js/core": "^0.8.
|
|
26
|
-
"@stone-js/env": "^0.8.
|
|
27
|
-
"@stone-js/filesystem": "^0.8.
|
|
28
|
-
"@stone-js/http-core": "^0.8.
|
|
29
|
-
"@stone-js/node-cli-adapter": "^0.8.
|
|
30
|
-
"@stone-js/node-http-adapter": "^0.8.
|
|
31
|
-
"@stone-js/pipeline": "^0.8.
|
|
32
|
-
"@stone-js/router": "^0.8.
|
|
25
|
+
"@stone-js/core": "^0.8.4",
|
|
26
|
+
"@stone-js/env": "^0.8.4",
|
|
27
|
+
"@stone-js/filesystem": "^0.8.4",
|
|
28
|
+
"@stone-js/http-core": "^0.8.4",
|
|
29
|
+
"@stone-js/node-cli-adapter": "^0.8.4",
|
|
30
|
+
"@stone-js/node-http-adapter": "^0.8.4",
|
|
31
|
+
"@stone-js/pipeline": "^0.8.4",
|
|
32
|
+
"@stone-js/router": "^0.8.4",
|
|
33
33
|
"bcryptjs": "^3.0.2",
|
|
34
34
|
"drizzle-orm": "^0.44.2",
|
|
35
35
|
"jsonwebtoken": "^9.0.2"
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@babel/plugin-proposal-decorators": "^7.25.9",
|
|
39
39
|
"@babel/preset-env": "^7.26.9",
|
|
40
40
|
"@babel/preset-typescript": "^7.27.0",
|
|
41
|
-
"@stone-js/cli": "^0.8.
|
|
41
|
+
"@stone-js/cli": "^0.8.4",
|
|
42
42
|
"@types/jsonwebtoken": "^9.0.8",
|
|
43
43
|
"@types/node": "^24.0.3",
|
|
44
44
|
"@vitest/coverage-v8": "^3.0.9",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { NotFoundError } from '@stone-js/http-core'
|
|
2
|
+
import { PostService } from '../app/services/PostService'
|
|
3
|
+
import { PostRepository } from '../app/repositories/PostRepository'
|
|
4
|
+
import { Post } from '../app/models/Post'
|
|
5
|
+
|
|
6
|
+
// Neutralize the @Service decorator to lighten the test environment,
|
|
7
|
+
// while keeping the real `isNotEmpty` helper so we assert real behavior.
|
|
8
|
+
vi.mock(import('@stone-js/core'), async (importOriginal) => {
|
|
9
|
+
const actual = await importOriginal()
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
Service: vi.fn(() => vi.fn()),
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('PostService', () => {
|
|
17
|
+
let service: PostService
|
|
18
|
+
let postRepository: PostRepository
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
postRepository = {
|
|
22
|
+
list: vi.fn(),
|
|
23
|
+
findById: vi.fn(),
|
|
24
|
+
} as unknown as PostRepository
|
|
25
|
+
|
|
26
|
+
service = new PostService({ postRepository })
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should delegate list() to the repository with the given limit', async () => {
|
|
30
|
+
// Arrange
|
|
31
|
+
const posts = [{ id: 1, title: 'Hello' }] as unknown as Post[]
|
|
32
|
+
;(postRepository.list as any).mockResolvedValue(posts)
|
|
33
|
+
|
|
34
|
+
// Act
|
|
35
|
+
const result = await service.list(5)
|
|
36
|
+
|
|
37
|
+
// Assert
|
|
38
|
+
expect(result).toBe(posts)
|
|
39
|
+
expect(postRepository.list).toHaveBeenCalledWith({ limit: 5 })
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should return the post when found and throw NotFoundError otherwise', async () => {
|
|
43
|
+
// Arrange
|
|
44
|
+
const post = { id: 42, title: 'Answer' } as unknown as Post
|
|
45
|
+
;(postRepository.findById as any).mockResolvedValueOnce(post)
|
|
46
|
+
|
|
47
|
+
// Act & Assert (found)
|
|
48
|
+
await expect(service.findById(42)).resolves.toBe(post)
|
|
49
|
+
|
|
50
|
+
// Arrange (not found)
|
|
51
|
+
;(postRepository.findById as any).mockResolvedValueOnce(undefined)
|
|
52
|
+
|
|
53
|
+
// Act & Assert (not found)
|
|
54
|
+
await expect(service.findById(99)).rejects.toBeInstanceOf(NotFoundError)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@libsql/client": "^0.14.0",
|
|
25
|
-
"@stone-js/core": "^0.8.
|
|
26
|
-
"@stone-js/env": "^0.8.
|
|
27
|
-
"@stone-js/filesystem": "^0.8.
|
|
28
|
-
"@stone-js/http-core": "^0.8.
|
|
29
|
-
"@stone-js/node-cli-adapter": "^0.8.
|
|
30
|
-
"@stone-js/node-http-adapter": "^0.8.
|
|
31
|
-
"@stone-js/pipeline": "^0.8.
|
|
32
|
-
"@stone-js/router": "^0.8.
|
|
25
|
+
"@stone-js/core": "^0.8.4",
|
|
26
|
+
"@stone-js/env": "^0.8.4",
|
|
27
|
+
"@stone-js/filesystem": "^0.8.4",
|
|
28
|
+
"@stone-js/http-core": "^0.8.4",
|
|
29
|
+
"@stone-js/node-cli-adapter": "^0.8.4",
|
|
30
|
+
"@stone-js/node-http-adapter": "^0.8.4",
|
|
31
|
+
"@stone-js/pipeline": "^0.8.4",
|
|
32
|
+
"@stone-js/router": "^0.8.4",
|
|
33
33
|
"bcryptjs": "^3.0.2",
|
|
34
34
|
"drizzle-orm": "^0.44.2",
|
|
35
35
|
"jsonwebtoken": "^9.0.2"
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@babel/plugin-proposal-decorators": "^7.25.9",
|
|
39
39
|
"@babel/preset-env": "^7.26.9",
|
|
40
40
|
"@babel/preset-typescript": "^7.27.0",
|
|
41
|
-
"@stone-js/cli": "^0.8.
|
|
41
|
+
"@stone-js/cli": "^0.8.4",
|
|
42
42
|
"@types/jsonwebtoken": "^9.0.8",
|
|
43
43
|
"@types/node": "^24.0.3",
|
|
44
44
|
"@vitest/coverage-v8": "^3.0.9",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { NotFoundError } from '@stone-js/http-core'
|
|
2
|
+
import { factoryPostService } from '../app/services/PostService'
|
|
3
|
+
import { IPostService } from '../app/services/contracts/IPostService'
|
|
4
|
+
import { IPostRepository } from '../app/repositories/contracts/IPostRepository'
|
|
5
|
+
import { Post } from '../app/models/Post'
|
|
6
|
+
|
|
7
|
+
describe('factoryPostService', () => {
|
|
8
|
+
let service: IPostService
|
|
9
|
+
let postRepository: IPostRepository
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
postRepository = {
|
|
13
|
+
list: vi.fn(),
|
|
14
|
+
findById: vi.fn(),
|
|
15
|
+
} as unknown as IPostRepository
|
|
16
|
+
|
|
17
|
+
service = factoryPostService({ postRepository })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('should delegate list() to the repository with the given limit', async () => {
|
|
21
|
+
// Arrange
|
|
22
|
+
const posts = [{ id: 1, title: 'Hello' }] as unknown as Post[]
|
|
23
|
+
;(postRepository.list as any).mockResolvedValue(posts)
|
|
24
|
+
|
|
25
|
+
// Act
|
|
26
|
+
const result = await service.list(5)
|
|
27
|
+
|
|
28
|
+
// Assert
|
|
29
|
+
expect(result).toBe(posts)
|
|
30
|
+
expect(postRepository.list).toHaveBeenCalledWith({ limit: 5 })
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should return the post when found and throw NotFoundError otherwise', async () => {
|
|
34
|
+
// Arrange
|
|
35
|
+
const post = { id: 42, title: 'Answer' } as unknown as Post
|
|
36
|
+
;(postRepository.findById as any).mockResolvedValueOnce(post)
|
|
37
|
+
|
|
38
|
+
// Act & Assert (found)
|
|
39
|
+
await expect(service.findById(42)).resolves.toBe(post)
|
|
40
|
+
|
|
41
|
+
// Arrange (not found)
|
|
42
|
+
;(postRepository.findById as any).mockResolvedValueOnce(undefined)
|
|
43
|
+
|
|
44
|
+
// Act & Assert (not found)
|
|
45
|
+
await expect(service.findById(99)).rejects.toBeInstanceOf(NotFoundError)
|
|
46
|
+
})
|
|
47
|
+
})
|
package/package.json
CHANGED
|
@@ -50,6 +50,7 @@ export class WelcomePage implements IPage<ReactIncomingEvent> {
|
|
|
50
50
|
render ({ data }: PageRenderContext<ResponseData>): JSX.Element {
|
|
51
51
|
return (
|
|
52
52
|
<section className='container'>
|
|
53
|
+
<img src='/logo.svg' alt='Stone.js' width={96} height={96} />
|
|
53
54
|
<h1 className='h1 text-center mt-64'>{data?.message}</h1>
|
|
54
55
|
</section>
|
|
55
56
|
)
|
|
@@ -20,19 +20,19 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"react": "^19.0.0",
|
|
22
22
|
"react-dom": "^19.0.0",
|
|
23
|
-
"@stone-js/use-react": "^0.8.
|
|
24
|
-
"@stone-js/browser-adapter": "^0.8.
|
|
25
|
-
"@stone-js/pipeline": "^0.8.
|
|
26
|
-
"@stone-js/http-core": "^0.8.
|
|
27
|
-
"@stone-js/node-http-adapter": "^0.8.
|
|
28
|
-
"@stone-js/router": "^0.8.
|
|
29
|
-
"@stone-js/core": "^0.8.
|
|
23
|
+
"@stone-js/use-react": "^0.8.4",
|
|
24
|
+
"@stone-js/browser-adapter": "^0.8.4",
|
|
25
|
+
"@stone-js/pipeline": "^0.8.4",
|
|
26
|
+
"@stone-js/http-core": "^0.8.4",
|
|
27
|
+
"@stone-js/node-http-adapter": "^0.8.4",
|
|
28
|
+
"@stone-js/router": "^0.8.4",
|
|
29
|
+
"@stone-js/core": "^0.8.4"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@babel/plugin-proposal-decorators": "^7.25.9",
|
|
33
33
|
"@babel/preset-env": "^7.26.9",
|
|
34
34
|
"@babel/preset-typescript": "^7.27.0",
|
|
35
|
-
"@stone-js/cli": "^0.8.
|
|
35
|
+
"@stone-js/cli": "^0.8.4",
|
|
36
36
|
"@types/node": "^24.0.7",
|
|
37
37
|
"@types/react": "^19.0.7",
|
|
38
38
|
"@types/react-dom": "^19.0.3",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" width="96" height="96" role="img" aria-label="Stone.js">
|
|
2
|
+
<title>Stone.js</title>
|
|
3
|
+
<style>
|
|
4
|
+
.ink { stroke: #1B1613; }
|
|
5
|
+
@media (prefers-color-scheme: dark) { .ink { stroke: #EFE8DC; } }
|
|
6
|
+
</style>
|
|
7
|
+
<defs>
|
|
8
|
+
<linearGradient id="braise" gradientUnits="userSpaceOnUse" x1="22" y1="14" x2="74" y2="26.1">
|
|
9
|
+
<stop offset="0" stop-color="#FFC46B" />
|
|
10
|
+
<stop offset="1" stop-color="#FF5A1F" />
|
|
11
|
+
</linearGradient>
|
|
12
|
+
</defs>
|
|
13
|
+
<path class="ink" d="M 79.96 36.4 A 34 34 0 0 1 53.9 81.5" fill="none" stroke-width="11" stroke-linecap="round" opacity=".85" />
|
|
14
|
+
<path class="ink" d="M 42.1 81.5 A 34 34 0 0 1 16.05 36.4" fill="none" stroke-width="11" stroke-linecap="round" opacity=".85" />
|
|
15
|
+
<path d="M 22 26.1 A 34 34 0 0 1 74 26.1" fill="none" stroke="url(#braise)" stroke-width="11" stroke-linecap="round" />
|
|
16
|
+
</svg>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ILogger } from '@stone-js/core'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { ReactIncomingEvent } from '@stone-js/use-react'
|
|
4
|
+
import { WelcomePage } from '../app/pages/welcome/WelcomePage'
|
|
5
|
+
import { WelcomeService } from '../app/services/WelcomeService'
|
|
6
|
+
|
|
7
|
+
// We must mock decorators to lighten the test environment
|
|
8
|
+
vi.mock('@stone-js/use-react', async (importOriginal) => {
|
|
9
|
+
const actual: any = await importOriginal()
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
Page: vi.fn(() => vi.fn()),
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
// We must mock decorators to lighten the test environment
|
|
17
|
+
vi.mock('@stone-js/core', async (importOriginal) => {
|
|
18
|
+
const actual: any = await importOriginal()
|
|
19
|
+
return {
|
|
20
|
+
...actual,
|
|
21
|
+
Service: vi.fn(() => vi.fn()),
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('WelcomePage', () => {
|
|
26
|
+
let page: WelcomePage
|
|
27
|
+
let mockedLogger: ILogger
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
mockedLogger = {
|
|
31
|
+
info: vi.fn(),
|
|
32
|
+
} as unknown as ILogger
|
|
33
|
+
|
|
34
|
+
page = new WelcomePage({ welcomeService: new WelcomeService({ logger: mockedLogger }) })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should handle incoming events', () => {
|
|
38
|
+
// Arrange
|
|
39
|
+
const expectedMessage = 'Hello World!'
|
|
40
|
+
const event = { get: () => 'World' } as unknown as ReactIncomingEvent
|
|
41
|
+
|
|
42
|
+
// Act
|
|
43
|
+
const response = page.handle(event)
|
|
44
|
+
|
|
45
|
+
// Assert
|
|
46
|
+
expect(response.message).toBe(expectedMessage)
|
|
47
|
+
expect(mockedLogger.info).toHaveBeenCalledWith('Welcome World')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should render the message', () => {
|
|
51
|
+
// Arrange
|
|
52
|
+
const message = 'Hello World!'
|
|
53
|
+
|
|
54
|
+
// Act
|
|
55
|
+
const response = renderToString(page.render({ data: { message } } as any))
|
|
56
|
+
|
|
57
|
+
// Assert
|
|
58
|
+
expect(response).toContain(message)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should render the Stone.js logo', () => {
|
|
62
|
+
// Act
|
|
63
|
+
const response = renderToString(page.render({ data: { message: 'Hello World!' } } as any))
|
|
64
|
+
|
|
65
|
+
// Assert
|
|
66
|
+
expect(response).toContain('/logo.svg')
|
|
67
|
+
expect(response).toContain('alt="Stone.js"')
|
|
68
|
+
})
|
|
69
|
+
})
|
|
@@ -38,6 +38,7 @@ export const WelcomePage = ({ welcomeService }: WelcomePageOptions): IPage<React
|
|
|
38
38
|
render ({ data }: PageRenderContext<ResponseData>): JSX.Element {
|
|
39
39
|
return (
|
|
40
40
|
<section className='container'>
|
|
41
|
+
<img src='/logo.svg' alt='Stone.js' width={96} height={96} />
|
|
41
42
|
<h1 className='h1 text-center mt-64'>{data?.message}</h1>
|
|
42
43
|
</section>
|
|
43
44
|
)
|
|
@@ -20,19 +20,19 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"react": "^19.0.0",
|
|
22
22
|
"react-dom": "^19.0.0",
|
|
23
|
-
"@stone-js/use-react": "^0.8.
|
|
24
|
-
"@stone-js/browser-adapter": "^0.8.
|
|
25
|
-
"@stone-js/pipeline": "^0.8.
|
|
26
|
-
"@stone-js/http-core": "^0.8.
|
|
27
|
-
"@stone-js/node-http-adapter": "^0.8.
|
|
28
|
-
"@stone-js/router": "^0.8.
|
|
29
|
-
"@stone-js/core": "^0.8.
|
|
23
|
+
"@stone-js/use-react": "^0.8.4",
|
|
24
|
+
"@stone-js/browser-adapter": "^0.8.4",
|
|
25
|
+
"@stone-js/pipeline": "^0.8.4",
|
|
26
|
+
"@stone-js/http-core": "^0.8.4",
|
|
27
|
+
"@stone-js/node-http-adapter": "^0.8.4",
|
|
28
|
+
"@stone-js/router": "^0.8.4",
|
|
29
|
+
"@stone-js/core": "^0.8.4"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@babel/plugin-proposal-decorators": "^7.25.9",
|
|
33
33
|
"@babel/preset-env": "^7.26.9",
|
|
34
34
|
"@babel/preset-typescript": "^7.27.0",
|
|
35
|
-
"@stone-js/cli": "^0.8.
|
|
35
|
+
"@stone-js/cli": "^0.8.4",
|
|
36
36
|
"@types/node": "^24.0.7",
|
|
37
37
|
"@types/react": "^19.0.7",
|
|
38
38
|
"@types/react-dom": "^19.0.3",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" width="96" height="96" role="img" aria-label="Stone.js">
|
|
2
|
+
<title>Stone.js</title>
|
|
3
|
+
<style>
|
|
4
|
+
.ink { stroke: #1B1613; }
|
|
5
|
+
@media (prefers-color-scheme: dark) { .ink { stroke: #EFE8DC; } }
|
|
6
|
+
</style>
|
|
7
|
+
<defs>
|
|
8
|
+
<linearGradient id="braise" gradientUnits="userSpaceOnUse" x1="22" y1="14" x2="74" y2="26.1">
|
|
9
|
+
<stop offset="0" stop-color="#FFC46B" />
|
|
10
|
+
<stop offset="1" stop-color="#FF5A1F" />
|
|
11
|
+
</linearGradient>
|
|
12
|
+
</defs>
|
|
13
|
+
<path class="ink" d="M 79.96 36.4 A 34 34 0 0 1 53.9 81.5" fill="none" stroke-width="11" stroke-linecap="round" opacity=".85" />
|
|
14
|
+
<path class="ink" d="M 42.1 81.5 A 34 34 0 0 1 16.05 36.4" fill="none" stroke-width="11" stroke-linecap="round" opacity=".85" />
|
|
15
|
+
<path d="M 22 26.1 A 34 34 0 0 1 74 26.1" fill="none" stroke="url(#braise)" stroke-width="11" stroke-linecap="round" />
|
|
16
|
+
</svg>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ILogger } from '@stone-js/core'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { ReactIncomingEvent } from '@stone-js/use-react'
|
|
4
|
+
import { WelcomePage } from '../app/pages/welcome/WelcomePage'
|
|
5
|
+
import { WelcomeService } from '../app/services/WelcomeService'
|
|
6
|
+
|
|
7
|
+
describe('WelcomePage', () => {
|
|
8
|
+
let page: any
|
|
9
|
+
let mockedLogger: ILogger
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockedLogger = {
|
|
13
|
+
info: vi.fn(),
|
|
14
|
+
} as unknown as ILogger
|
|
15
|
+
|
|
16
|
+
page = WelcomePage({ welcomeService: WelcomeService({ logger: mockedLogger }) })
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should handle incoming events', () => {
|
|
20
|
+
// Arrange
|
|
21
|
+
const expectedMessage = 'Hello World!'
|
|
22
|
+
const event = { get: () => 'World' } as unknown as ReactIncomingEvent
|
|
23
|
+
|
|
24
|
+
// Act
|
|
25
|
+
const response = page.handle(event) as { message: string }
|
|
26
|
+
|
|
27
|
+
// Assert
|
|
28
|
+
expect(response.message).toBe(expectedMessage)
|
|
29
|
+
expect(mockedLogger.info).toHaveBeenCalledWith('Welcome World')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should render the message', () => {
|
|
33
|
+
// Arrange
|
|
34
|
+
const message = 'Hello World!'
|
|
35
|
+
|
|
36
|
+
// Act
|
|
37
|
+
const response = renderToString(page.render({ data: { message } } as any))
|
|
38
|
+
|
|
39
|
+
// Assert
|
|
40
|
+
expect(response).toContain(message)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should render the Stone.js logo', () => {
|
|
44
|
+
// Act
|
|
45
|
+
const response = renderToString(page.render({ data: { message: 'Hello World!' } } as any))
|
|
46
|
+
|
|
47
|
+
// Assert
|
|
48
|
+
expect(response).toContain('/logo.svg')
|
|
49
|
+
expect(response).toContain('alt="Stone.js"')
|
|
50
|
+
})
|
|
51
|
+
})
|