sunpeak 0.18.14 → 0.19.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.
Files changed (53) hide show
  1. package/README.md +37 -134
  2. package/bin/commands/new.mjs +3 -1
  3. package/bin/commands/test-init.mjs +305 -0
  4. package/bin/commands/test.mjs +144 -0
  5. package/bin/lib/inspect/inspect-config.d.mts +4 -0
  6. package/bin/lib/inspect/inspect-config.mjs +18 -24
  7. package/bin/lib/test/base-config.mjs +75 -0
  8. package/bin/lib/test/matchers.mjs +99 -0
  9. package/bin/lib/test/test-config.d.mts +66 -0
  10. package/bin/lib/test/test-config.mjs +125 -0
  11. package/bin/lib/test/test-fixtures.d.mts +129 -0
  12. package/bin/lib/test/test-fixtures.mjs +232 -0
  13. package/bin/sunpeak.js +18 -5
  14. package/package.json +22 -10
  15. package/template/README.md +18 -8
  16. package/template/dist/albums/albums.json +1 -1
  17. package/template/dist/carousel/carousel.json +1 -1
  18. package/template/dist/map/map.html +468 -280
  19. package/template/dist/map/map.json +1 -1
  20. package/template/dist/review/review.json +1 -1
  21. package/template/node_modules/.bin/playwright +2 -2
  22. package/template/node_modules/.bin/vite +2 -2
  23. package/template/node_modules/.bin/vitest +2 -2
  24. package/template/node_modules/.vite/deps/_metadata.json +4 -4
  25. package/template/node_modules/.vite-mcp/deps/_metadata.json +22 -22
  26. package/template/node_modules/.vite-mcp/deps/mapbox-gl.js +15924 -14588
  27. package/template/node_modules/.vite-mcp/deps/mapbox-gl.js.map +1 -1
  28. package/template/node_modules/.vite-mcp/deps/vitest.js +8 -8
  29. package/template/node_modules/.vite-mcp/deps/vitest.js.map +1 -1
  30. package/template/package.json +9 -7
  31. package/template/playwright.config.ts +2 -40
  32. package/template/test-results/.last-run.json +4 -0
  33. package/template/tests/e2e/albums.spec.ts +114 -245
  34. package/template/tests/e2e/carousel.spec.ts +189 -313
  35. package/template/tests/e2e/map.spec.ts +177 -300
  36. package/template/tests/e2e/review.spec.ts +232 -423
  37. package/template/tests/e2e/visual.spec.ts +36 -0
  38. package/template/tests/e2e/visual.spec.ts-snapshots/albums-dark-chatgpt-linux.png +0 -0
  39. package/template/tests/e2e/visual.spec.ts-snapshots/albums-dark-claude-linux.png +0 -0
  40. package/template/tests/e2e/visual.spec.ts-snapshots/albums-fullscreen-chatgpt-linux.png +0 -0
  41. package/template/tests/e2e/visual.spec.ts-snapshots/albums-fullscreen-claude-linux.png +0 -0
  42. package/template/tests/e2e/visual.spec.ts-snapshots/albums-light-chatgpt-linux.png +0 -0
  43. package/template/tests/e2e/visual.spec.ts-snapshots/albums-light-claude-linux.png +0 -0
  44. package/template/tests/e2e/visual.spec.ts-snapshots/albums-page-light-chatgpt-linux.png +0 -0
  45. package/template/tests/e2e/visual.spec.ts-snapshots/albums-page-light-claude-linux.png +0 -0
  46. package/template/tests/live/albums.spec.ts +1 -1
  47. package/template/tests/live/carousel.spec.ts +1 -1
  48. package/template/tests/live/map.spec.ts +1 -1
  49. package/template/tests/live/playwright.config.ts +1 -1
  50. package/template/tests/live/review.spec.ts +1 -1
  51. package/template/vitest.config.ts +1 -1
  52. package/template/tests/e2e/global-setup.ts +0 -10
  53. package/template/tests/e2e/helpers.ts +0 -13
@@ -7,30 +7,32 @@
7
7
  "dev": "sunpeak dev",
8
8
  "build": "sunpeak build",
9
9
  "start": "sunpeak start",
10
- "test": "vitest run",
11
- "test:e2e": "playwright test",
12
- "test:live": "playwright test --config tests/live/playwright.config.ts"
10
+ "test": "sunpeak test",
11
+ "test:unit": "sunpeak test --unit",
12
+ "test:e2e": "sunpeak test --e2e",
13
+ "test:visual": "sunpeak test --visual",
14
+ "test:live": "sunpeak test --live"
13
15
  },
14
16
  "dependencies": {
15
17
  "clsx": "^2.1.1",
16
18
  "embla-carousel-react": "^8.6.0",
17
19
  "embla-carousel-wheel-gestures": "^8.1.0",
18
- "mapbox-gl": "^3.20.0",
20
+ "mapbox-gl": "^3.21.0",
19
21
  "sunpeak": "workspace:*",
20
22
  "tailwind-merge": "^3.5.0",
21
23
  "zod": "^4.3.6"
22
24
  },
23
25
  "devDependencies": {
24
- "@playwright/test": "^1.58.2",
26
+ "@playwright/test": "^1.59.1",
25
27
  "@tailwindcss/vite": "^4.2.2",
26
28
  "@testing-library/jest-dom": "^6.9.1",
27
29
  "@testing-library/react": "^16.3.2",
28
30
  "@testing-library/user-event": "^14.6.1",
29
- "@types/node": "^25.5.0",
31
+ "@types/node": "^25.5.2",
30
32
  "@types/react": "^19.2.14",
31
33
  "@types/react-dom": "^19.2.3",
32
34
  "@vitejs/plugin-react": "^6.0.1",
33
- "jsdom": "^29.0.1",
35
+ "happy-dom": "^18.0.1",
34
36
  "react": "^19.2.4",
35
37
  "react-dom": "^19.2.4",
36
38
  "tailwindcss": "^4.2.2",
@@ -1,41 +1,3 @@
1
- import { defineConfig, devices } from '@playwright/test';
1
+ import { defineConfig } from 'sunpeak/test/config';
2
2
 
3
- // Use fixed preferred ports. If in use, `reuseExistingServer` (local) reuses
4
- // the running server. In CI, validate.mjs assigns unique ports via env vars.
5
- const port = Number(process.env.SUNPEAK_TEST_PORT) || 6776;
6
- const sandboxPort = Number(process.env.SUNPEAK_SANDBOX_PORT) || 24680;
7
-
8
- export default defineConfig({
9
- globalSetup: './tests/e2e/global-setup.ts',
10
- testDir: './tests/e2e',
11
- fullyParallel: true,
12
- forbidOnly: !!process.env.CI,
13
- retries: process.env.CI ? 2 : 1,
14
- // Limit parallel workers. Each test loads a full inspector page with
15
- // iframe → cross-origin sandbox proxy → inner iframe. Too many concurrent
16
- // pages overwhelm the sandbox proxy server and cause PostMessage relay
17
- // timeouts. 2 workers balances speed vs reliability.
18
- workers: process.env.CI ? 1 : 2,
19
- reporter: 'list',
20
- use: {
21
- baseURL: `http://localhost:${port}`,
22
- trace: 'on-first-retry',
23
- },
24
- projects: [
25
- {
26
- name: 'chromium',
27
- use: {
28
- ...devices['Desktop Chrome'],
29
- launchOptions: {
30
- args: ['--use-gl=angle'],
31
- },
32
- },
33
- },
34
- ],
35
- webServer: {
36
- command: `PORT=${port} SUNPEAK_SANDBOX_PORT=${sandboxPort} pnpm dev`,
37
- url: `http://localhost:${port}/health`,
38
- reuseExistingServer: !process.env.CI,
39
- timeout: 60000,
40
- },
41
- });
3
+ export default defineConfig();
@@ -0,0 +1,4 @@
1
+ {
2
+ "status": "passed",
3
+ "failedTests": []
4
+ }
@@ -1,246 +1,115 @@
1
- import { test, expect } from '@playwright/test';
2
- import { createInspectorUrl } from './helpers';
3
-
4
- const hosts = ['chatgpt', 'claude'] as const;
5
-
6
- for (const host of hosts) {
7
- test.describe(`Albums Resource [${host}]`, () => {
8
- test.describe('Light Mode', () => {
9
- test('should render album cards with correct styles', async ({ page }) => {
10
- await page.goto(createInspectorUrl({ simulation: 'show-albums', theme: 'light', host }));
11
-
12
- const iframe = page.frameLocator('iframe').frameLocator('iframe');
13
- const albumCard = iframe.locator('button:has-text("Summer Slice")');
14
- await expect(albumCard).toBeVisible();
15
-
16
- // Verify album card unique styles
17
- const styles = await albumCard.evaluate((el) => {
18
- const computed = window.getComputedStyle(el);
19
- return {
20
- cursor: computed.cursor,
21
- borderRadius: computed.borderRadius,
22
- };
23
- });
24
-
25
- expect(styles.cursor).toBe('pointer');
26
- expect(styles.borderRadius).toBe('12px'); // rounded-xl
27
- });
28
-
29
- test('should have album image with correct aspect ratio', async ({ page }) => {
30
- await page.goto(createInspectorUrl({ simulation: 'show-albums', theme: 'light', host }));
31
-
32
- const iframe = page.frameLocator('iframe').frameLocator('iframe');
33
- const albumImage = iframe.locator('button:has-text("Summer Slice") img').first();
34
- await expect(albumImage).toBeVisible();
35
-
36
- // Verify aspect-[4/3] container
37
- const imageContainer = iframe.locator(
38
- 'button:has-text("Summer Slice") .aspect-\\[4\\/3\\]'
39
- );
40
- await expect(imageContainer).toBeVisible();
41
-
42
- const containerStyles = await imageContainer.evaluate((el) => {
43
- const computed = window.getComputedStyle(el);
44
- return {
45
- borderRadius: computed.borderRadius,
46
- overflow: computed.overflow,
47
- };
48
- });
49
-
50
- expect(containerStyles.borderRadius).toBe('12px'); // rounded-xl
51
- expect(containerStyles.overflow).toBe('hidden');
52
- });
53
- });
54
-
55
- test.describe('Dark Mode', () => {
56
- test('should render album cards with correct styles', async ({ page }) => {
57
- await page.goto(createInspectorUrl({ simulation: 'show-albums', theme: 'dark', host }));
58
-
59
- const iframe = page.frameLocator('iframe').frameLocator('iframe');
60
- const albumCard = iframe.locator('button:has-text("Summer Slice")');
61
- await expect(albumCard).toBeVisible();
62
-
63
- const styles = await albumCard.evaluate((el) => {
64
- const computed = window.getComputedStyle(el);
65
- return {
66
- cursor: computed.cursor,
67
- borderRadius: computed.borderRadius,
68
- };
69
- });
70
-
71
- expect(styles.cursor).toBe('pointer');
72
- expect(styles.borderRadius).toBe('12px'); // rounded-xl
73
- });
74
-
75
- test('should have text with appropriate contrast', async ({ page }) => {
76
- await page.goto(createInspectorUrl({ simulation: 'show-albums', theme: 'dark', host }));
77
-
78
- const iframe = page.frameLocator('iframe').frameLocator('iframe');
79
- const albumTitle = iframe.locator('button:has-text("Summer Slice") div').first();
80
- await expect(albumTitle).toBeVisible();
81
-
82
- // In dark mode, text should be light colored for contrast
83
- const titleStyles = await albumTitle.evaluate((el) => {
84
- const computed = window.getComputedStyle(el);
85
- return {
86
- color: computed.color,
87
- };
88
- });
89
-
90
- // Verify the text color exists (should be a light color in dark mode)
91
- expect(titleStyles.color).toBeTruthy();
92
- });
93
- });
94
-
95
- test.describe('Prod Tools Mode', () => {
96
- test('should show empty state with Run button', async ({ page }) => {
97
- await page.goto(createInspectorUrl({ tool: 'show-albums', theme: 'dark', host }));
98
-
99
- // Should show the "Press Run to call the tool" empty state
100
- const emptyState = page.locator('text=Press Run to call the tool');
101
- await expect(emptyState).toBeVisible();
102
-
103
- // Run button should be visible in the conversation header
104
- const runButton = page.locator('button:has-text("Run")');
105
- await expect(runButton).toBeVisible();
106
-
107
- // Iframe should NOT be present (no resource loaded yet)
108
- const iframe = page.locator('iframe');
109
- await expect(iframe).not.toBeAttached();
110
- });
111
-
112
- test('should have themed empty state colors in light mode', async ({ page }) => {
113
- await page.goto(createInspectorUrl({ tool: 'show-albums', theme: 'light', host }));
114
-
115
- const emptyState = page.locator('text=Press Run to call the tool');
116
- await expect(emptyState).toBeVisible();
117
-
118
- const color = await emptyState.evaluate((el) => {
119
- return window.getComputedStyle(el).color;
120
- });
121
-
122
- // Light mode text-secondary should be a dark-ish color (not white/very light)
123
- const [r, g, b] = color.match(/\d+/g)!.map(Number);
124
- // In light mode, secondary text should have a reasonable luminance (not too bright)
125
- expect(r + g + b).toBeLessThan(600);
126
- });
127
-
128
- test('should have themed empty state colors in dark mode', async ({ page }) => {
129
- await page.goto(createInspectorUrl({ tool: 'show-albums', theme: 'dark', host }));
130
-
131
- const emptyState = page.locator('text=Press Run to call the tool');
132
- await expect(emptyState).toBeVisible();
133
-
134
- const color = await emptyState.evaluate((el) => {
135
- return window.getComputedStyle(el).color;
136
- });
137
-
138
- // Dark mode text-secondary should be a light-ish color (not black/very dark)
139
- const [r, g, b] = color.match(/\d+/g)!.map(Number);
140
- expect(r + g + b).toBeGreaterThan(200);
141
- });
142
- });
143
-
144
- test.describe('Prod Resources Mode', () => {
145
- test('should render resource normally when dist is available', async ({ page }) => {
146
- // With prodResources=true but no dist/ files, shows "Building..."
147
- // With dist/ files available, renders the resource from dist/
148
- // This test verifies the mode activates without errors
149
- await page.goto(
150
- createInspectorUrl({
151
- simulation: 'show-albums',
152
- theme: 'dark',
153
- host,
154
- prodResources: true,
155
- })
156
- );
157
-
158
- // Should either show "Building..." or the resource (depending on dist availability)
159
- const root = page.locator('#root');
160
- await expect(root).not.toBeEmpty();
161
- });
162
- });
163
-
164
- test.describe('Fullscreen Mode', () => {
165
- test('should render correctly in fullscreen displayMode', async ({ page }) => {
166
- await page.goto(
167
- createInspectorUrl({
168
- simulation: 'show-albums',
169
- theme: 'light',
170
- displayMode: 'fullscreen',
171
- host,
172
- })
173
- );
174
-
175
- // Wait for content to load
176
- await page.waitForLoadState('networkidle');
177
-
178
- // The root container should be present
179
- const root = page.locator('#root');
180
- await expect(root).not.toBeEmpty();
181
- });
182
-
183
- test('should maintain album card styles in fullscreen', async ({ page }) => {
184
- await page.goto(
185
- createInspectorUrl({
186
- simulation: 'show-albums',
187
- theme: 'dark',
188
- displayMode: 'fullscreen',
189
- host,
190
- })
191
- );
192
-
193
- const iframe = page.frameLocator('iframe').frameLocator('iframe');
194
- const albumCard = iframe.locator('button:has-text("Summer Slice")');
195
- await expect(albumCard).toBeVisible();
196
-
197
- const styles = await albumCard.evaluate((el) => {
198
- const computed = window.getComputedStyle(el);
199
- return {
200
- cursor: computed.cursor,
201
- borderRadius: computed.borderRadius,
202
- };
203
- });
204
-
205
- expect(styles.cursor).toBe('pointer');
206
- expect(styles.borderRadius).toBe('12px');
207
- });
208
-
209
- test('should render content after switching from inline to fullscreen', async ({ page }) => {
210
- // Start in inline mode
211
- await page.goto(createInspectorUrl({ simulation: 'show-albums', theme: 'dark', host }));
212
-
213
- const iframe = page.frameLocator('iframe').frameLocator('iframe');
214
- await expect(iframe.locator('button:has-text("Summer Slice")')).toBeVisible();
215
-
216
- // Switch to fullscreen via sidebar
217
- await page.locator('button:has-text("Full")').click();
218
-
219
- // Content should still be visible after the mode transition
220
- await expect(iframe.locator('button:has-text("Summer Slice")')).toBeVisible({
221
- timeout: 5000,
222
- });
223
- });
224
-
225
- // Claude doesn't support PiP — only run this test for hosts that have the button.
226
- (host === 'claude' ? test.skip : test)(
227
- 'should render content after switching from inline to pip',
228
- async ({ page }) => {
229
- // Start in inline mode
230
- await page.goto(createInspectorUrl({ simulation: 'show-albums', theme: 'dark', host }));
231
-
232
- const iframe = page.frameLocator('iframe').frameLocator('iframe');
233
- await expect(iframe.locator('button:has-text("Summer Slice")')).toBeVisible();
234
-
235
- // Switch to PiP via sidebar
236
- await page.locator('button:has-text("PiP")').click();
237
-
238
- // Content should still be visible after the mode transition
239
- await expect(iframe.locator('button:has-text("Summer Slice")')).toBeVisible({
240
- timeout: 5000,
241
- });
242
- }
243
- );
244
- });
1
+ import { test, expect } from 'sunpeak/test';
2
+
3
+ test('should render album cards with correct styles', async ({ mcp }) => {
4
+ const result = await mcp.callTool('show-albums');
5
+ const app = result.app();
6
+
7
+ const albumCard = app.locator('button:has-text("Summer Slice")');
8
+ await expect(albumCard).toBeVisible();
9
+
10
+ const styles = await albumCard.evaluate((el) => {
11
+ const computed = window.getComputedStyle(el);
12
+ return { cursor: computed.cursor, borderRadius: computed.borderRadius };
13
+ });
14
+ expect(styles.cursor).toBe('pointer');
15
+ expect(styles.borderRadius).toBe('12px');
16
+ });
17
+
18
+ test('should have album image with correct aspect ratio', async ({ mcp }) => {
19
+ const result = await mcp.callTool('show-albums');
20
+ const app = result.app();
21
+
22
+ const imageContainer = app.locator('button:has-text("Summer Slice") .aspect-\\[4\\/3\\]');
23
+ await expect(imageContainer).toBeVisible();
24
+
25
+ const styles = await imageContainer.evaluate((el) => {
26
+ const computed = window.getComputedStyle(el);
27
+ return { borderRadius: computed.borderRadius, overflow: computed.overflow };
245
28
  });
246
- }
29
+ expect(styles.borderRadius).toBe('12px');
30
+ expect(styles.overflow).toBe('hidden');
31
+ });
32
+
33
+ test('should render album cards in dark mode', async ({ mcp }) => {
34
+ const result = await mcp.callTool('show-albums', {}, { theme: 'dark' });
35
+ const app = result.app();
36
+
37
+ const albumTitle = app.locator('button:has-text("Summer Slice") div').first();
38
+ await expect(albumTitle).toBeVisible();
39
+
40
+ const titleStyles = await albumTitle.evaluate((el) => ({
41
+ color: window.getComputedStyle(el).color,
42
+ }));
43
+ expect(titleStyles.color).toBeTruthy();
44
+ });
45
+
46
+ test('should show empty state with Run button in prod tools mode', async ({ mcp }) => {
47
+ await mcp.openTool('show-albums', { theme: 'dark' });
48
+
49
+ await expect(mcp.page.locator('text=Press Run to call the tool')).toBeVisible();
50
+ await expect(mcp.page.locator('button:has-text("Run")')).toBeVisible();
51
+ await expect(mcp.page.locator('iframe')).not.toBeAttached();
52
+ });
53
+
54
+ test('should have themed empty state colors in light mode', async ({ mcp }) => {
55
+ await mcp.openTool('show-albums', { theme: 'light' });
56
+
57
+ const emptyState = mcp.page.locator('text=Press Run to call the tool');
58
+ await expect(emptyState).toBeVisible();
59
+
60
+ const color = await emptyState.evaluate((el) => window.getComputedStyle(el).color);
61
+ const [r, g, b] = color.match(/\d+/g)!.map(Number);
62
+ expect(r + g + b).toBeLessThan(600);
63
+ });
64
+
65
+ test('should have themed empty state colors in dark mode', async ({ mcp }) => {
66
+ await mcp.openTool('show-albums', { theme: 'dark' });
67
+
68
+ const emptyState = mcp.page.locator('text=Press Run to call the tool');
69
+ await expect(emptyState).toBeVisible();
70
+
71
+ const color = await emptyState.evaluate((el) => window.getComputedStyle(el).color);
72
+ const [r, g, b] = color.match(/\d+/g)!.map(Number);
73
+ expect(r + g + b).toBeGreaterThan(200);
74
+ });
75
+
76
+ test('should activate prod resources mode without errors', async ({ mcp }) => {
77
+ await mcp.callTool('show-albums', {}, { theme: 'dark', prodResources: true });
78
+ const root = mcp.page.locator('#root');
79
+ await expect(root).not.toBeEmpty();
80
+ });
81
+
82
+ test('should render correctly in fullscreen', async ({ mcp }) => {
83
+ const result = await mcp.callTool('show-albums', {}, { displayMode: 'fullscreen' });
84
+ const app = result.app();
85
+
86
+ const albumCard = app.locator('button:has-text("Summer Slice")');
87
+ await expect(albumCard).toBeVisible();
88
+
89
+ const styles = await albumCard.evaluate((el) => ({
90
+ cursor: window.getComputedStyle(el).cursor,
91
+ borderRadius: window.getComputedStyle(el).borderRadius,
92
+ }));
93
+ expect(styles.cursor).toBe('pointer');
94
+ expect(styles.borderRadius).toBe('12px');
95
+ });
96
+
97
+ test('should preserve content when switching to fullscreen', async ({ mcp }) => {
98
+ const result = await mcp.callTool('show-albums', {}, { theme: 'dark' });
99
+ const app = result.app();
100
+ await expect(app.locator('button:has-text("Summer Slice")')).toBeVisible();
101
+
102
+ await mcp.setDisplayMode('fullscreen');
103
+ await expect(app.locator('button:has-text("Summer Slice")')).toBeVisible({ timeout: 5000 });
104
+ });
105
+
106
+ test('should preserve content when switching to PiP', async ({ mcp }) => {
107
+ test.skip(mcp.host === 'claude', 'Claude does not support PiP');
108
+
109
+ const result = await mcp.callTool('show-albums', {}, { theme: 'dark' });
110
+ const app = result.app();
111
+ await expect(app.locator('button:has-text("Summer Slice")')).toBeVisible();
112
+
113
+ await mcp.setDisplayMode('pip');
114
+ await expect(app.locator('button:has-text("Summer Slice")')).toBeVisible({ timeout: 5000 });
115
+ });