@hustle-together/api-dev-tools 3.6.5 → 3.9.2
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/README.md +5307 -258
- package/bin/cli.js +348 -20
- package/commands/README.md +459 -71
- package/commands/hustle-api-continue.md +158 -0
- package/commands/{api-create.md → hustle-api-create.md} +22 -2
- package/commands/{api-env.md → hustle-api-env.md} +4 -4
- package/commands/{api-interview.md → hustle-api-interview.md} +1 -1
- package/commands/{api-research.md → hustle-api-research.md} +3 -3
- package/commands/hustle-api-sessions.md +149 -0
- package/commands/{api-status.md → hustle-api-status.md} +16 -16
- package/commands/{api-verify.md → hustle-api-verify.md} +2 -2
- package/commands/hustle-combine.md +763 -0
- package/commands/hustle-ui-create.md +825 -0
- package/hooks/api-workflow-check.py +385 -19
- package/hooks/cache-research.py +337 -0
- package/hooks/check-playwright-setup.py +103 -0
- package/hooks/check-storybook-setup.py +81 -0
- package/hooks/detect-interruption.py +165 -0
- package/hooks/enforce-brand-guide.py +131 -0
- package/hooks/enforce-documentation.py +60 -8
- package/hooks/enforce-freshness.py +184 -0
- package/hooks/enforce-questions-sourced.py +146 -0
- package/hooks/enforce-schema-from-interview.py +248 -0
- package/hooks/enforce-ui-disambiguation.py +108 -0
- package/hooks/enforce-ui-interview.py +130 -0
- package/hooks/generate-manifest-entry.py +981 -0
- package/hooks/session-logger.py +297 -0
- package/hooks/session-startup.py +65 -10
- package/hooks/track-scope-coverage.py +220 -0
- package/hooks/track-tool-use.py +81 -1
- package/hooks/update-api-showcase.py +149 -0
- package/hooks/update-registry.py +352 -0
- package/hooks/update-ui-showcase.py +148 -0
- package/package.json +8 -2
- package/templates/BRAND_GUIDE.md +299 -0
- package/templates/CLAUDE-SECTION.md +56 -24
- package/templates/SPEC.json +640 -0
- package/templates/api-dev-state.json +179 -161
- package/templates/api-showcase/APICard.tsx +153 -0
- package/templates/api-showcase/APIModal.tsx +375 -0
- package/templates/api-showcase/APIShowcase.tsx +231 -0
- package/templates/api-showcase/APITester.tsx +522 -0
- package/templates/api-showcase/page.tsx +41 -0
- package/templates/component/Component.stories.tsx +172 -0
- package/templates/component/Component.test.tsx +237 -0
- package/templates/component/Component.tsx +86 -0
- package/templates/component/Component.types.ts +55 -0
- package/templates/component/index.ts +15 -0
- package/templates/dev-tools/_components/DevToolsLanding.tsx +320 -0
- package/templates/dev-tools/page.tsx +10 -0
- package/templates/page/page.e2e.test.ts +218 -0
- package/templates/page/page.tsx +42 -0
- package/templates/performance-budgets.json +58 -0
- package/templates/registry.json +13 -0
- package/templates/settings.json +74 -0
- package/templates/shared/HeroHeader.tsx +261 -0
- package/templates/shared/index.ts +1 -0
- package/templates/ui-showcase/PreviewCard.tsx +315 -0
- package/templates/ui-showcase/PreviewModal.tsx +676 -0
- package/templates/ui-showcase/UIShowcase.tsx +262 -0
- package/templates/ui-showcase/page.tsx +26 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { HeroHeader } from '../../shared/HeroHeader';
|
|
6
|
+
|
|
7
|
+
interface Registry {
|
|
8
|
+
version?: string;
|
|
9
|
+
apis?: Record<string, unknown>;
|
|
10
|
+
components?: Record<string, unknown>;
|
|
11
|
+
pages?: Record<string, unknown>;
|
|
12
|
+
combined?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* DevToolsLanding Component
|
|
17
|
+
*
|
|
18
|
+
* Central hub for all Hustle Developer Tools.
|
|
19
|
+
* Links to API Showcase, UI Showcase, and displays registry stats.
|
|
20
|
+
*
|
|
21
|
+
* Created with Hustle Dev Tools (v3.9.2)
|
|
22
|
+
*/
|
|
23
|
+
export function DevToolsLanding() {
|
|
24
|
+
const [registry, setRegistry] = useState<Registry>({});
|
|
25
|
+
const [loading, setLoading] = useState(true);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
fetch('/api/registry')
|
|
29
|
+
.then((res) => res.json())
|
|
30
|
+
.then((data) => {
|
|
31
|
+
setRegistry(data);
|
|
32
|
+
setLoading(false);
|
|
33
|
+
})
|
|
34
|
+
.catch(() => {
|
|
35
|
+
setLoading(false);
|
|
36
|
+
});
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
const stats = {
|
|
40
|
+
apis: Object.keys(registry.apis || {}).length,
|
|
41
|
+
components: Object.keys(registry.components || {}).length,
|
|
42
|
+
pages: Object.keys(registry.pages || {}).length,
|
|
43
|
+
combined: Object.keys(registry.combined || {}).length,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const total = stats.apis + stats.components + stats.pages + stats.combined;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="min-h-screen bg-white dark:bg-gray-950">
|
|
50
|
+
<HeroHeader
|
|
51
|
+
title="Hustle Dev Tools"
|
|
52
|
+
badge="Developer Portal"
|
|
53
|
+
description={
|
|
54
|
+
<>
|
|
55
|
+
Central hub for <strong>API development</strong>,{' '}
|
|
56
|
+
<strong>UI components</strong>, and documentation. Built with the
|
|
57
|
+
Hustle Together workflow.
|
|
58
|
+
</>
|
|
59
|
+
}
|
|
60
|
+
/>
|
|
61
|
+
|
|
62
|
+
<main className="mx-auto max-w-6xl px-8 py-12 md:px-16">
|
|
63
|
+
{/* Stats Bar */}
|
|
64
|
+
<div className="mb-8 flex flex-wrap items-center gap-6 border-2 border-black bg-gray-50 p-6 dark:border-gray-700 dark:bg-gray-900">
|
|
65
|
+
<div className="flex items-center gap-2">
|
|
66
|
+
<span className="text-3xl font-bold text-black dark:text-white">
|
|
67
|
+
{loading ? '...' : total}
|
|
68
|
+
</span>
|
|
69
|
+
<span className="text-gray-600 dark:text-gray-400">
|
|
70
|
+
Total Items
|
|
71
|
+
</span>
|
|
72
|
+
</div>
|
|
73
|
+
<div className="h-8 w-px bg-gray-300 dark:bg-gray-600" />
|
|
74
|
+
<div className="flex items-center gap-2">
|
|
75
|
+
<span className="font-bold text-black dark:text-white">
|
|
76
|
+
{stats.apis}
|
|
77
|
+
</span>
|
|
78
|
+
<span className="text-gray-600 dark:text-gray-400">APIs</span>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="flex items-center gap-2">
|
|
81
|
+
<span className="font-bold text-black dark:text-white">
|
|
82
|
+
{stats.components}
|
|
83
|
+
</span>
|
|
84
|
+
<span className="text-gray-600 dark:text-gray-400">Components</span>
|
|
85
|
+
</div>
|
|
86
|
+
<div className="flex items-center gap-2">
|
|
87
|
+
<span className="font-bold text-black dark:text-white">
|
|
88
|
+
{stats.pages}
|
|
89
|
+
</span>
|
|
90
|
+
<span className="text-gray-600 dark:text-gray-400">Pages</span>
|
|
91
|
+
</div>
|
|
92
|
+
<div className="flex items-center gap-2">
|
|
93
|
+
<span className="font-bold text-black dark:text-white">
|
|
94
|
+
{stats.combined}
|
|
95
|
+
</span>
|
|
96
|
+
<span className="text-gray-600 dark:text-gray-400">Combined</span>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{/* Main Cards Grid */}
|
|
101
|
+
<div className="mb-12 grid gap-6 md:grid-cols-3">
|
|
102
|
+
{/* API Showcase Card */}
|
|
103
|
+
<Link
|
|
104
|
+
href="/api-showcase"
|
|
105
|
+
className="group flex flex-col border-2 border-black bg-white p-6 transition-all hover:border-[#BA0C2F] hover:shadow-[4px_4px_0px_0px_rgba(186,12,47,0.2)] dark:border-gray-700 dark:bg-gray-900"
|
|
106
|
+
>
|
|
107
|
+
<div className="mb-4 flex h-12 w-12 items-center justify-center border-2 border-[#BA0C2F] bg-[#BA0C2F]/10">
|
|
108
|
+
<svg
|
|
109
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
110
|
+
width="24"
|
|
111
|
+
height="24"
|
|
112
|
+
viewBox="0 0 24 24"
|
|
113
|
+
fill="none"
|
|
114
|
+
stroke="currentColor"
|
|
115
|
+
strokeWidth="2"
|
|
116
|
+
strokeLinecap="round"
|
|
117
|
+
strokeLinejoin="round"
|
|
118
|
+
className="text-[#BA0C2F]"
|
|
119
|
+
>
|
|
120
|
+
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
|
|
121
|
+
<polyline points="22,6 12,13 2,6" />
|
|
122
|
+
</svg>
|
|
123
|
+
</div>
|
|
124
|
+
<h2 className="mb-2 text-xl font-bold text-black group-hover:text-[#BA0C2F] dark:text-white">
|
|
125
|
+
API Showcase
|
|
126
|
+
</h2>
|
|
127
|
+
<p className="mb-4 flex-1 text-sm text-gray-600 dark:text-gray-400">
|
|
128
|
+
Interactive testing and documentation for all API endpoints. Try
|
|
129
|
+
requests, view schemas, and copy curl commands.
|
|
130
|
+
</p>
|
|
131
|
+
<div className="flex items-center gap-2 text-sm font-bold text-[#BA0C2F]">
|
|
132
|
+
<span>{stats.apis} APIs</span>
|
|
133
|
+
<span>+</span>
|
|
134
|
+
<span>{stats.combined} Combined</span>
|
|
135
|
+
</div>
|
|
136
|
+
</Link>
|
|
137
|
+
|
|
138
|
+
{/* UI Showcase Card */}
|
|
139
|
+
<Link
|
|
140
|
+
href="/ui-showcase"
|
|
141
|
+
className="group flex flex-col border-2 border-black bg-white p-6 transition-all hover:border-[#BA0C2F] hover:shadow-[4px_4px_0px_0px_rgba(186,12,47,0.2)] dark:border-gray-700 dark:bg-gray-900"
|
|
142
|
+
>
|
|
143
|
+
<div className="mb-4 flex h-12 w-12 items-center justify-center border-2 border-[#BA0C2F] bg-[#BA0C2F]/10">
|
|
144
|
+
<svg
|
|
145
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
146
|
+
width="24"
|
|
147
|
+
height="24"
|
|
148
|
+
viewBox="0 0 24 24"
|
|
149
|
+
fill="none"
|
|
150
|
+
stroke="currentColor"
|
|
151
|
+
strokeWidth="2"
|
|
152
|
+
strokeLinecap="round"
|
|
153
|
+
strokeLinejoin="round"
|
|
154
|
+
className="text-[#BA0C2F]"
|
|
155
|
+
>
|
|
156
|
+
<rect width="7" height="7" x="3" y="3" rx="1" />
|
|
157
|
+
<rect width="7" height="7" x="14" y="3" rx="1" />
|
|
158
|
+
<rect width="7" height="7" x="14" y="14" rx="1" />
|
|
159
|
+
<rect width="7" height="7" x="3" y="14" rx="1" />
|
|
160
|
+
</svg>
|
|
161
|
+
</div>
|
|
162
|
+
<h2 className="mb-2 text-xl font-bold text-black group-hover:text-[#BA0C2F] dark:text-white">
|
|
163
|
+
UI Showcase
|
|
164
|
+
</h2>
|
|
165
|
+
<p className="mb-4 flex-1 text-sm text-gray-600 dark:text-gray-400">
|
|
166
|
+
Live component previews with Sandpack. Edit code in real-time,
|
|
167
|
+
switch variants, and test responsive layouts.
|
|
168
|
+
</p>
|
|
169
|
+
<div className="flex items-center gap-2 text-sm font-bold text-[#BA0C2F]">
|
|
170
|
+
<span>{stats.components} Components</span>
|
|
171
|
+
<span>+</span>
|
|
172
|
+
<span>{stats.pages} Pages</span>
|
|
173
|
+
</div>
|
|
174
|
+
</Link>
|
|
175
|
+
|
|
176
|
+
{/* Registry Card */}
|
|
177
|
+
<Link
|
|
178
|
+
href="/api/registry"
|
|
179
|
+
target="_blank"
|
|
180
|
+
className="group flex flex-col border-2 border-black bg-white p-6 transition-all hover:border-[#BA0C2F] hover:shadow-[4px_4px_0px_0px_rgba(186,12,47,0.2)] dark:border-gray-700 dark:bg-gray-900"
|
|
181
|
+
>
|
|
182
|
+
<div className="mb-4 flex h-12 w-12 items-center justify-center border-2 border-[#BA0C2F] bg-[#BA0C2F]/10">
|
|
183
|
+
<svg
|
|
184
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
185
|
+
width="24"
|
|
186
|
+
height="24"
|
|
187
|
+
viewBox="0 0 24 24"
|
|
188
|
+
fill="none"
|
|
189
|
+
stroke="currentColor"
|
|
190
|
+
strokeWidth="2"
|
|
191
|
+
strokeLinecap="round"
|
|
192
|
+
strokeLinejoin="round"
|
|
193
|
+
className="text-[#BA0C2F]"
|
|
194
|
+
>
|
|
195
|
+
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
|
196
|
+
<polyline points="14 2 14 8 20 8" />
|
|
197
|
+
<line x1="16" x2="8" y1="13" y2="13" />
|
|
198
|
+
<line x1="16" x2="8" y1="17" y2="17" />
|
|
199
|
+
<line x1="10" x2="8" y1="9" y2="9" />
|
|
200
|
+
</svg>
|
|
201
|
+
</div>
|
|
202
|
+
<h2 className="mb-2 text-xl font-bold text-black group-hover:text-[#BA0C2F] dark:text-white">
|
|
203
|
+
Registry JSON
|
|
204
|
+
</h2>
|
|
205
|
+
<p className="mb-4 flex-1 text-sm text-gray-600 dark:text-gray-400">
|
|
206
|
+
Raw JSON registry of all APIs, components, and pages. Central
|
|
207
|
+
source of truth for the showcase pages.
|
|
208
|
+
</p>
|
|
209
|
+
<div className="text-sm font-bold text-[#BA0C2F]">
|
|
210
|
+
Version: {registry.version || '1.0.0'}
|
|
211
|
+
</div>
|
|
212
|
+
</Link>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{/* Quick Actions */}
|
|
216
|
+
<div className="border-2 border-black bg-gray-50 p-6 dark:border-gray-700 dark:bg-gray-900">
|
|
217
|
+
<h3 className="mb-4 text-lg font-bold text-black dark:text-white">
|
|
218
|
+
Quick Actions
|
|
219
|
+
</h3>
|
|
220
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
221
|
+
<QuickAction
|
|
222
|
+
title="Run Storybook"
|
|
223
|
+
command="pnpm storybook"
|
|
224
|
+
description="Component development server"
|
|
225
|
+
/>
|
|
226
|
+
<QuickAction
|
|
227
|
+
title="Run Tests"
|
|
228
|
+
command="pnpm test"
|
|
229
|
+
description="Execute test suite"
|
|
230
|
+
/>
|
|
231
|
+
<QuickAction
|
|
232
|
+
title="Create API"
|
|
233
|
+
command="/hustle-api-create"
|
|
234
|
+
description="Start new API workflow"
|
|
235
|
+
/>
|
|
236
|
+
<QuickAction
|
|
237
|
+
title="Create Component"
|
|
238
|
+
command="/hustle-ui-create"
|
|
239
|
+
description="Start new UI workflow"
|
|
240
|
+
/>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
{/* Setup Instructions */}
|
|
245
|
+
<div className="mt-8 border-2 border-black bg-white p-6 dark:border-gray-700 dark:bg-gray-900">
|
|
246
|
+
<h3 className="mb-4 text-lg font-bold text-black dark:text-white">
|
|
247
|
+
Optional Setup
|
|
248
|
+
</h3>
|
|
249
|
+
<div className="grid gap-4 md:grid-cols-3">
|
|
250
|
+
<SetupCard
|
|
251
|
+
title="Storybook"
|
|
252
|
+
command="npx storybook@latest init"
|
|
253
|
+
description="Interactive component development and visual testing"
|
|
254
|
+
/>
|
|
255
|
+
<SetupCard
|
|
256
|
+
title="Playwright"
|
|
257
|
+
command="npm init playwright@latest"
|
|
258
|
+
description="E2E testing and accessibility verification"
|
|
259
|
+
/>
|
|
260
|
+
<SetupCard
|
|
261
|
+
title="Sandpack"
|
|
262
|
+
command="pnpm add @codesandbox/sandpack-react"
|
|
263
|
+
description="Live code previews in UI Showcase"
|
|
264
|
+
/>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</main>
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function QuickAction({
|
|
273
|
+
title,
|
|
274
|
+
command,
|
|
275
|
+
description,
|
|
276
|
+
}: {
|
|
277
|
+
title: string;
|
|
278
|
+
command: string;
|
|
279
|
+
description: string;
|
|
280
|
+
}) {
|
|
281
|
+
return (
|
|
282
|
+
<div className="border-2 border-black bg-white p-4 dark:border-gray-600 dark:bg-gray-800">
|
|
283
|
+
<h4 className="font-bold text-black dark:text-white">{title}</h4>
|
|
284
|
+
<code className="mt-2 block border border-gray-300 bg-gray-100 px-2 py-1 font-mono text-sm text-gray-700 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300">
|
|
285
|
+
{command}
|
|
286
|
+
</code>
|
|
287
|
+
<p className="mt-2 text-xs text-gray-600 dark:text-gray-400">
|
|
288
|
+
{description}
|
|
289
|
+
</p>
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function SetupCard({
|
|
295
|
+
title,
|
|
296
|
+
command,
|
|
297
|
+
description,
|
|
298
|
+
}: {
|
|
299
|
+
title: string;
|
|
300
|
+
command: string;
|
|
301
|
+
description: string;
|
|
302
|
+
}) {
|
|
303
|
+
return (
|
|
304
|
+
<div className="flex flex-col border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800">
|
|
305
|
+
<h4 className="font-bold text-black dark:text-white">{title}</h4>
|
|
306
|
+
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
307
|
+
{description}
|
|
308
|
+
</p>
|
|
309
|
+
<button
|
|
310
|
+
onClick={() => navigator.clipboard.writeText(command)}
|
|
311
|
+
className="mt-3 border-2 border-black bg-white px-3 py-1.5 text-left font-mono text-sm text-gray-700 transition-colors hover:border-[#BA0C2F] hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
|
312
|
+
>
|
|
313
|
+
{command}
|
|
314
|
+
<span className="float-right text-gray-400">Copy</span>
|
|
315
|
+
</button>
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export default DevToolsLanding;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { DevToolsLanding } from './_components/DevToolsLanding';
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
title: 'Hustle Dev Tools',
|
|
5
|
+
description: 'Developer tools for API and UI development with Hustle Together',
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default function DevToolsPage() {
|
|
9
|
+
return <DevToolsLanding />;
|
|
10
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* E2E Tests for __PAGE_NAME__ Page
|
|
5
|
+
*
|
|
6
|
+
* Created with Hustle UI Create workflow (v3.9.0)
|
|
7
|
+
*
|
|
8
|
+
* Run with: pnpm playwright test __PAGE_ROUTE__.spec.ts
|
|
9
|
+
*/
|
|
10
|
+
test.describe('__PAGE_NAME__ Page', () => {
|
|
11
|
+
test.beforeEach(async ({ page }) => {
|
|
12
|
+
// Navigate to the page before each test
|
|
13
|
+
await page.goto('/__PAGE_ROUTE__');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// ===================================
|
|
17
|
+
// Basic Rendering Tests
|
|
18
|
+
// ===================================
|
|
19
|
+
|
|
20
|
+
test('should load successfully', async ({ page }) => {
|
|
21
|
+
// Wait for page to be fully loaded
|
|
22
|
+
await page.waitForLoadState('networkidle');
|
|
23
|
+
|
|
24
|
+
// Check page title
|
|
25
|
+
await expect(page).toHaveTitle(/__PAGE_TITLE__/);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('should display page heading', async ({ page }) => {
|
|
29
|
+
const heading = page.getByRole('heading', { level: 1 });
|
|
30
|
+
await expect(heading).toBeVisible();
|
|
31
|
+
await expect(heading).toContainText('__PAGE_TITLE__');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('should display page description', async ({ page }) => {
|
|
35
|
+
const description = page.getByText('__PAGE_DESCRIPTION__');
|
|
36
|
+
await expect(description).toBeVisible();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ===================================
|
|
40
|
+
// Responsive Tests
|
|
41
|
+
// ===================================
|
|
42
|
+
|
|
43
|
+
test('should be responsive on mobile', async ({ page }) => {
|
|
44
|
+
await page.setViewportSize({ width: 375, height: 667 });
|
|
45
|
+
await page.goto('/__PAGE_ROUTE__');
|
|
46
|
+
|
|
47
|
+
// Verify main content is visible
|
|
48
|
+
await expect(page.getByRole('main')).toBeVisible();
|
|
49
|
+
|
|
50
|
+
// Verify no horizontal scroll
|
|
51
|
+
const body = await page.locator('body');
|
|
52
|
+
const scrollWidth = await body.evaluate((el) => el.scrollWidth);
|
|
53
|
+
const clientWidth = await body.evaluate((el) => el.clientWidth);
|
|
54
|
+
expect(scrollWidth).toBeLessThanOrEqual(clientWidth + 1); // +1 for rounding
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('should be responsive on tablet', async ({ page }) => {
|
|
58
|
+
await page.setViewportSize({ width: 768, height: 1024 });
|
|
59
|
+
await page.goto('/__PAGE_ROUTE__');
|
|
60
|
+
|
|
61
|
+
await expect(page.getByRole('main')).toBeVisible();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('should be responsive on desktop', async ({ page }) => {
|
|
65
|
+
await page.setViewportSize({ width: 1920, height: 1080 });
|
|
66
|
+
await page.goto('/__PAGE_ROUTE__');
|
|
67
|
+
|
|
68
|
+
await expect(page.getByRole('main')).toBeVisible();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ===================================
|
|
72
|
+
// Accessibility Tests
|
|
73
|
+
// ===================================
|
|
74
|
+
|
|
75
|
+
test('should have no accessibility violations', async ({ page }) => {
|
|
76
|
+
// Note: Requires @axe-core/playwright
|
|
77
|
+
// const results = await new AxeBuilder({ page }).analyze();
|
|
78
|
+
// expect(results.violations).toEqual([]);
|
|
79
|
+
|
|
80
|
+
// Basic accessibility checks
|
|
81
|
+
// All images should have alt text
|
|
82
|
+
const images = await page.getByRole('img').all();
|
|
83
|
+
for (const img of images) {
|
|
84
|
+
await expect(img).toHaveAttribute('alt');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// All buttons should have accessible names
|
|
88
|
+
const buttons = await page.getByRole('button').all();
|
|
89
|
+
for (const button of buttons) {
|
|
90
|
+
const name = await button.getAttribute('aria-label') || await button.textContent();
|
|
91
|
+
expect(name?.trim()).toBeTruthy();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('should be keyboard navigable', async ({ page }) => {
|
|
96
|
+
// Tab through interactive elements
|
|
97
|
+
await page.keyboard.press('Tab');
|
|
98
|
+
|
|
99
|
+
// Verify focus is visible
|
|
100
|
+
const focusedElement = page.locator(':focus');
|
|
101
|
+
await expect(focusedElement).toBeVisible();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ===================================
|
|
105
|
+
// Performance Tests (TDD GATES)
|
|
106
|
+
// These thresholds match .claude/performance-budgets.json
|
|
107
|
+
// Tests FAIL if exceeded, triggering TDD loop-back
|
|
108
|
+
// ===================================
|
|
109
|
+
|
|
110
|
+
test('should load within performance budget', async ({ page }) => {
|
|
111
|
+
const startTime = Date.now();
|
|
112
|
+
await page.goto('/__PAGE_ROUTE__', { waitUntil: 'networkidle' });
|
|
113
|
+
const loadTime = Date.now() - startTime;
|
|
114
|
+
|
|
115
|
+
// THRESHOLD: Page load max 3000ms
|
|
116
|
+
// If this fails, optimize: code splitting, lazy loading, reduce bundle
|
|
117
|
+
expect(loadTime).toBeLessThan(3000);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('should have acceptable memory usage', async ({ page }) => {
|
|
121
|
+
await page.goto('/__PAGE_ROUTE__');
|
|
122
|
+
|
|
123
|
+
// Get Chromium-specific metrics
|
|
124
|
+
const client = await page.context().newCDPSession(page);
|
|
125
|
+
const metrics = await client.send('Performance.getMetrics');
|
|
126
|
+
|
|
127
|
+
const jsHeapSize = metrics.metrics.find(m => m.name === 'JSHeapUsedSize')?.value || 0;
|
|
128
|
+
const domNodes = metrics.metrics.find(m => m.name === 'Nodes')?.value || 0;
|
|
129
|
+
|
|
130
|
+
// THRESHOLD: Memory max 50MB
|
|
131
|
+
// If this fails, check for: memory leaks, large state, unbounded lists
|
|
132
|
+
expect(jsHeapSize).toBeLessThan(50 * 1024 * 1024);
|
|
133
|
+
|
|
134
|
+
// THRESHOLD: DOM nodes max 1500
|
|
135
|
+
// If this fails, check for: unnecessary renders, infinite lists without virtualization
|
|
136
|
+
expect(domNodes).toBeLessThan(1500);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('should not have layout thrashing', async ({ page }) => {
|
|
140
|
+
await page.goto('/__PAGE_ROUTE__');
|
|
141
|
+
|
|
142
|
+
const client = await page.context().newCDPSession(page);
|
|
143
|
+
const metrics = await client.send('Performance.getMetrics');
|
|
144
|
+
|
|
145
|
+
const layoutCount = metrics.metrics.find(m => m.name === 'LayoutCount')?.value || 0;
|
|
146
|
+
const layoutDuration = metrics.metrics.find(m => m.name === 'LayoutDuration')?.value || 0;
|
|
147
|
+
|
|
148
|
+
// THRESHOLD: Layout count max 10
|
|
149
|
+
// If this fails, batch DOM updates, use CSS transforms instead of layout properties
|
|
150
|
+
expect(layoutCount).toBeLessThan(10);
|
|
151
|
+
|
|
152
|
+
// THRESHOLD: Layout duration max 100ms
|
|
153
|
+
expect(layoutDuration * 1000).toBeLessThan(100);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('should meet Core Web Vitals', async ({ page }) => {
|
|
157
|
+
await page.goto('/__PAGE_ROUTE__');
|
|
158
|
+
|
|
159
|
+
// Get paint timing
|
|
160
|
+
const paintTiming = await page.evaluate(() => {
|
|
161
|
+
const entries = performance.getEntriesByType('paint');
|
|
162
|
+
return {
|
|
163
|
+
fcp: entries.find(e => e.name === 'first-contentful-paint')?.startTime || 0,
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// THRESHOLD: First Contentful Paint max 1500ms
|
|
168
|
+
// If this fails, optimize critical rendering path
|
|
169
|
+
expect(paintTiming.fcp).toBeLessThan(1500);
|
|
170
|
+
|
|
171
|
+
// Get LCP via PerformanceObserver result (if available)
|
|
172
|
+
const lcp = await page.evaluate(() => {
|
|
173
|
+
return new Promise<number>((resolve) => {
|
|
174
|
+
new PerformanceObserver((list) => {
|
|
175
|
+
const entries = list.getEntries();
|
|
176
|
+
const lastEntry = entries[entries.length - 1];
|
|
177
|
+
resolve(lastEntry?.startTime || 0);
|
|
178
|
+
}).observe({ type: 'largest-contentful-paint', buffered: true });
|
|
179
|
+
|
|
180
|
+
// Timeout fallback
|
|
181
|
+
setTimeout(() => resolve(0), 3000);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// THRESHOLD: Largest Contentful Paint max 2500ms
|
|
186
|
+
if (lcp > 0) {
|
|
187
|
+
expect(lcp).toBeLessThan(2500);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ===================================
|
|
192
|
+
// Feature-Specific Tests
|
|
193
|
+
// ===================================
|
|
194
|
+
|
|
195
|
+
// TODO: Add tests for specific page features
|
|
196
|
+
// Example:
|
|
197
|
+
// test('should display user data when logged in', async ({ page }) => {
|
|
198
|
+
// // Setup auth state
|
|
199
|
+
// // await page.context().addCookies([...]);
|
|
200
|
+
//
|
|
201
|
+
// await page.goto('/__PAGE_ROUTE__');
|
|
202
|
+
// await expect(page.getByText('Welcome')).toBeVisible();
|
|
203
|
+
// });
|
|
204
|
+
|
|
205
|
+
// ===================================
|
|
206
|
+
// Error Handling Tests
|
|
207
|
+
// ===================================
|
|
208
|
+
|
|
209
|
+
test('should handle errors gracefully', async ({ page }) => {
|
|
210
|
+
// Intercept API calls to simulate errors (if applicable)
|
|
211
|
+
// await page.route('/api/*', route => route.fulfill({ status: 500 }));
|
|
212
|
+
|
|
213
|
+
await page.goto('/__PAGE_ROUTE__');
|
|
214
|
+
|
|
215
|
+
// Page should still render without crashing
|
|
216
|
+
await expect(page.getByRole('main')).toBeVisible();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Page metadata for SEO
|
|
5
|
+
*/
|
|
6
|
+
export const metadata: Metadata = {
|
|
7
|
+
title: '__PAGE_TITLE__',
|
|
8
|
+
description: '__PAGE_DESCRIPTION__',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* __PAGE_NAME__ Page
|
|
13
|
+
*
|
|
14
|
+
* @description __PAGE_DESCRIPTION__
|
|
15
|
+
*
|
|
16
|
+
* Created with Hustle UI Create workflow (v3.9.0)
|
|
17
|
+
*/
|
|
18
|
+
export default async function __PAGE_NAME__Page() {
|
|
19
|
+
// Server-side data fetching (if needed)
|
|
20
|
+
// const data = await fetchData();
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<main className="container mx-auto px-4 py-8">
|
|
24
|
+
{/* Page Header */}
|
|
25
|
+
<header className="mb-8">
|
|
26
|
+
<h1 className="text-3xl font-bold tracking-tight">__PAGE_TITLE__</h1>
|
|
27
|
+
<p className="mt-2 text-muted-foreground">__PAGE_DESCRIPTION__</p>
|
|
28
|
+
</header>
|
|
29
|
+
|
|
30
|
+
{/* Page Content */}
|
|
31
|
+
<section className="space-y-6">
|
|
32
|
+
{/* Add your components here */}
|
|
33
|
+
<div className="rounded-lg border bg-card p-6">
|
|
34
|
+
<h2 className="text-xl font-semibold">Getting Started</h2>
|
|
35
|
+
<p className="mt-2 text-muted-foreground">
|
|
36
|
+
Replace this content with your page implementation.
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
</section>
|
|
40
|
+
</main>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"description": "Performance budgets for UI components and pages. Tests will FAIL if these thresholds are exceeded.",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
|
|
6
|
+
"memory": {
|
|
7
|
+
"description": "Memory usage thresholds (in MB)",
|
|
8
|
+
"page_max_mb": 50,
|
|
9
|
+
"component_max_mb": 10,
|
|
10
|
+
"heap_growth_max_mb": 5
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
"renders": {
|
|
14
|
+
"description": "React render count thresholds",
|
|
15
|
+
"mount_max": 1,
|
|
16
|
+
"prop_change_max": 1,
|
|
17
|
+
"state_change_max": 1,
|
|
18
|
+
"unnecessary_renders_max": 0
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
"layout": {
|
|
22
|
+
"description": "Layout performance thresholds",
|
|
23
|
+
"count_max": 10,
|
|
24
|
+
"duration_max_ms": 100,
|
|
25
|
+
"style_recalc_max": 20
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
"timing": {
|
|
29
|
+
"description": "Load and interaction timing thresholds (in ms)",
|
|
30
|
+
"page_load_max_ms": 3000,
|
|
31
|
+
"first_contentful_paint_max_ms": 1500,
|
|
32
|
+
"time_to_interactive_max_ms": 3500,
|
|
33
|
+
"largest_contentful_paint_max_ms": 2500
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
"bundle": {
|
|
37
|
+
"description": "Bundle size thresholds (in KB)",
|
|
38
|
+
"component_max_kb": 50,
|
|
39
|
+
"page_max_kb": 200,
|
|
40
|
+
"chunk_max_kb": 100
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
"accessibility": {
|
|
44
|
+
"description": "Accessibility requirements",
|
|
45
|
+
"wcag_level": "AA",
|
|
46
|
+
"violations_max": 0,
|
|
47
|
+
"color_contrast_min": 4.5
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
"responsive": {
|
|
51
|
+
"description": "Responsive breakpoints to test",
|
|
52
|
+
"breakpoints": [
|
|
53
|
+
{ "name": "mobile", "width": 375, "height": 667 },
|
|
54
|
+
{ "name": "tablet", "width": 768, "height": 1024 },
|
|
55
|
+
{ "name": "desktop", "width": 1920, "height": 1080 }
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
}
|