sunpeak 0.8.8 → 0.9.1

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 (60) hide show
  1. package/bin/sunpeak.js +4 -0
  2. package/dist/chatgpt/chatgpt-simulator.d.ts +1 -2
  3. package/dist/chatgpt/index.cjs +9 -0
  4. package/dist/chatgpt/index.cjs.map +1 -0
  5. package/dist/chatgpt/index.d.ts +3 -1
  6. package/dist/chatgpt/index.js +9 -0
  7. package/dist/chatgpt/index.js.map +1 -0
  8. package/dist/chatgpt/simulator-url.d.ts +127 -0
  9. package/dist/index.cjs +39 -8448
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.js +58 -8466
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp/entry.cjs +2 -1
  14. package/dist/mcp/entry.cjs.map +1 -1
  15. package/dist/mcp/entry.js +2 -1
  16. package/dist/mcp/entry.js.map +1 -1
  17. package/dist/mcp/index.cjs +1 -1
  18. package/dist/mcp/index.d.ts +1 -1
  19. package/dist/mcp/index.js +1 -1
  20. package/dist/mcp/server.d.ts +1 -1
  21. package/dist/mcp/types.d.ts +3 -9
  22. package/dist/{server-DVmTC-SF.js → server-310A1k9o.js} +2 -2
  23. package/dist/{server-DVmTC-SF.js.map → server-310A1k9o.js.map} +1 -1
  24. package/dist/{server-B9YgCQdS.cjs → server-CSybLAYo.cjs} +2 -2
  25. package/dist/{server-B9YgCQdS.cjs.map → server-CSybLAYo.cjs.map} +1 -1
  26. package/dist/simulator-url-CG8lAAC3.cjs +8545 -0
  27. package/dist/simulator-url-CG8lAAC3.cjs.map +1 -0
  28. package/dist/simulator-url-CexnaL-e.js +8529 -0
  29. package/dist/simulator-url-CexnaL-e.js.map +1 -0
  30. package/dist/style.css +5858 -5843
  31. package/dist/types/simulation.d.ts +6 -32
  32. package/package.json +7 -3
  33. package/template/.sunpeak/dev.tsx +6 -5
  34. package/template/dist/albums.js +1 -1
  35. package/template/dist/albums.json +1 -1
  36. package/template/dist/carousel.js +1 -1
  37. package/template/dist/carousel.json +1 -1
  38. package/template/dist/map.js +1 -1
  39. package/template/dist/map.json +1 -1
  40. package/template/dist/review.js +2 -2
  41. package/template/dist/review.json +1 -1
  42. package/template/node_modules/.bin/playwright +21 -0
  43. package/template/node_modules/.vite/deps/_metadata.json +22 -22
  44. package/template/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  45. package/template/package.json +3 -1
  46. package/template/playwright.config.ts +26 -0
  47. package/template/src/resources/review-resource.test.tsx +2 -2
  48. package/template/src/resources/review-resource.tsx +6 -1
  49. package/template/src/simulations/albums-show-simulation.json +1 -1
  50. package/template/src/simulations/carousel-show-simulation.json +1 -1
  51. package/template/src/simulations/map-show-simulation.json +1 -1
  52. package/template/src/simulations/review-diff-simulation.json +1 -1
  53. package/template/src/simulations/review-post-simulation.json +1 -1
  54. package/template/src/simulations/review-purchase-simulation.json +1 -1
  55. package/template/test-results/.last-run.json +4 -0
  56. package/template/tests/e2e/albums.spec.ts +120 -0
  57. package/template/tests/e2e/carousel.spec.ts +127 -0
  58. package/template/tests/e2e/map.spec.ts +188 -0
  59. package/template/tests/e2e/review.spec.ts +245 -0
  60. package/template/vitest.config.ts +1 -0
@@ -12,5 +12,5 @@
12
12
  ]
13
13
  }
14
14
  },
15
- "uri": "ui://review-mjw4w0g9"
15
+ "uri": "ui://review-mk0a1cc5"
16
16
  }
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/home/runner/work/sunpeak/sunpeak/node_modules/.pnpm/@playwright+test@1.57.0/node_modules/@playwright/test/node_modules:/home/runner/work/sunpeak/sunpeak/node_modules/.pnpm/@playwright+test@1.57.0/node_modules/@playwright/node_modules:/home/runner/work/sunpeak/sunpeak/node_modules/.pnpm/@playwright+test@1.57.0/node_modules:/home/runner/work/sunpeak/sunpeak/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/home/runner/work/sunpeak/sunpeak/node_modules/.pnpm/@playwright+test@1.57.0/node_modules/@playwright/test/node_modules:/home/runner/work/sunpeak/sunpeak/node_modules/.pnpm/@playwright+test@1.57.0/node_modules/@playwright/node_modules:/home/runner/work/sunpeak/sunpeak/node_modules/.pnpm/@playwright+test@1.57.0/node_modules:/home/runner/work/sunpeak/sunpeak/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../@playwright/test/cli.js" "$@"
19
+ else
20
+ exec node "$basedir/../@playwright/test/cli.js" "$@"
21
+ fi
@@ -1,121 +1,121 @@
1
1
  {
2
- "hash": "2f940a3d",
2
+ "hash": "b74c9763",
3
3
  "configHash": "f56e5903",
4
- "lockfileHash": "bdba07c2",
5
- "browserHash": "0bd02dfa",
4
+ "lockfileHash": "8ed69ad2",
5
+ "browserHash": "238bb651",
6
6
  "optimized": {
7
7
  "react": {
8
8
  "src": "../../../../node_modules/.pnpm/react@19.2.3/node_modules/react/index.js",
9
9
  "file": "react.js",
10
- "fileHash": "d6aa24ec",
10
+ "fileHash": "8a2b29af",
11
11
  "needsInterop": true
12
12
  },
13
13
  "react-dom": {
14
14
  "src": "../../../../node_modules/.pnpm/react-dom@19.2.3_react@19.2.3/node_modules/react-dom/index.js",
15
15
  "file": "react-dom.js",
16
- "fileHash": "a84166b8",
16
+ "fileHash": "02e92ec7",
17
17
  "needsInterop": true
18
18
  },
19
19
  "react/jsx-dev-runtime": {
20
20
  "src": "../../../../node_modules/.pnpm/react@19.2.3/node_modules/react/jsx-dev-runtime.js",
21
21
  "file": "react_jsx-dev-runtime.js",
22
- "fileHash": "92c4ccf8",
22
+ "fileHash": "5af9d5c8",
23
23
  "needsInterop": true
24
24
  },
25
25
  "react/jsx-runtime": {
26
26
  "src": "../../../../node_modules/.pnpm/react@19.2.3/node_modules/react/jsx-runtime.js",
27
27
  "file": "react_jsx-runtime.js",
28
- "fileHash": "fee8e947",
28
+ "fileHash": "f58e269f",
29
29
  "needsInterop": true
30
30
  },
31
31
  "@openai/apps-sdk-ui/components/Avatar": {
32
32
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.1_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._90324f97b7190ccfdbe40a9e8bef3385/node_modules/@openai/apps-sdk-ui/dist/es/components/Avatar/index.js",
33
33
  "file": "@openai_apps-sdk-ui_components_Avatar.js",
34
- "fileHash": "d1d0268f",
34
+ "fileHash": "0be1731c",
35
35
  "needsInterop": false
36
36
  },
37
37
  "@openai/apps-sdk-ui/components/Button": {
38
38
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.1_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._90324f97b7190ccfdbe40a9e8bef3385/node_modules/@openai/apps-sdk-ui/dist/es/components/Button/index.js",
39
39
  "file": "@openai_apps-sdk-ui_components_Button.js",
40
- "fileHash": "0d815226",
40
+ "fileHash": "0794a7a4",
41
41
  "needsInterop": false
42
42
  },
43
43
  "@openai/apps-sdk-ui/components/Checkbox": {
44
44
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.1_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._90324f97b7190ccfdbe40a9e8bef3385/node_modules/@openai/apps-sdk-ui/dist/es/components/Checkbox/index.js",
45
45
  "file": "@openai_apps-sdk-ui_components_Checkbox.js",
46
- "fileHash": "e87c4e4e",
46
+ "fileHash": "e9409941",
47
47
  "needsInterop": false
48
48
  },
49
49
  "@openai/apps-sdk-ui/components/Icon": {
50
50
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.1_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._90324f97b7190ccfdbe40a9e8bef3385/node_modules/@openai/apps-sdk-ui/dist/es/components/Icon/index.js",
51
51
  "file": "@openai_apps-sdk-ui_components_Icon.js",
52
- "fileHash": "5c946825",
52
+ "fileHash": "33cf5a6a",
53
53
  "needsInterop": false
54
54
  },
55
55
  "@openai/apps-sdk-ui/components/Input": {
56
56
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.1_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._90324f97b7190ccfdbe40a9e8bef3385/node_modules/@openai/apps-sdk-ui/dist/es/components/Input/index.js",
57
57
  "file": "@openai_apps-sdk-ui_components_Input.js",
58
- "fileHash": "e5d8b16c",
58
+ "fileHash": "6442a396",
59
59
  "needsInterop": false
60
60
  },
61
61
  "@openai/apps-sdk-ui/components/SegmentedControl": {
62
62
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.1_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._90324f97b7190ccfdbe40a9e8bef3385/node_modules/@openai/apps-sdk-ui/dist/es/components/SegmentedControl/index.js",
63
63
  "file": "@openai_apps-sdk-ui_components_SegmentedControl.js",
64
- "fileHash": "4494f051",
64
+ "fileHash": "a4bb0813",
65
65
  "needsInterop": false
66
66
  },
67
67
  "@openai/apps-sdk-ui/components/Select": {
68
68
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.1_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._90324f97b7190ccfdbe40a9e8bef3385/node_modules/@openai/apps-sdk-ui/dist/es/components/Select/index.js",
69
69
  "file": "@openai_apps-sdk-ui_components_Select.js",
70
- "fileHash": "e67a1bb8",
70
+ "fileHash": "b9f54262",
71
71
  "needsInterop": false
72
72
  },
73
73
  "@openai/apps-sdk-ui/components/Textarea": {
74
74
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.1_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._90324f97b7190ccfdbe40a9e8bef3385/node_modules/@openai/apps-sdk-ui/dist/es/components/Textarea/index.js",
75
75
  "file": "@openai_apps-sdk-ui_components_Textarea.js",
76
- "fileHash": "c9eb4f91",
76
+ "fileHash": "bdab6c1c",
77
77
  "needsInterop": false
78
78
  },
79
79
  "@openai/apps-sdk-ui/theme": {
80
80
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.1_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._90324f97b7190ccfdbe40a9e8bef3385/node_modules/@openai/apps-sdk-ui/dist/es/lib/theme.js",
81
81
  "file": "@openai_apps-sdk-ui_theme.js",
82
- "fileHash": "8d57b3ed",
82
+ "fileHash": "78560b8d",
83
83
  "needsInterop": false
84
84
  },
85
85
  "clsx": {
86
86
  "src": "../../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs",
87
87
  "file": "clsx.js",
88
- "fileHash": "e10bef76",
88
+ "fileHash": "7c6c6e97",
89
89
  "needsInterop": false
90
90
  },
91
91
  "embla-carousel-react": {
92
92
  "src": "../../../../node_modules/.pnpm/embla-carousel-react@8.6.0_react@19.2.3/node_modules/embla-carousel-react/esm/embla-carousel-react.esm.js",
93
93
  "file": "embla-carousel-react.js",
94
- "fileHash": "6d9c07d5",
94
+ "fileHash": "a791d56c",
95
95
  "needsInterop": false
96
96
  },
97
97
  "embla-carousel-wheel-gestures": {
98
98
  "src": "../../../../node_modules/.pnpm/embla-carousel-wheel-gestures@8.1.0_embla-carousel@8.6.0/node_modules/embla-carousel-wheel-gestures/dist/embla-carousel-wheel-gestures.esm.js",
99
99
  "file": "embla-carousel-wheel-gestures.js",
100
- "fileHash": "3f6f688e",
100
+ "fileHash": "456a567a",
101
101
  "needsInterop": false
102
102
  },
103
103
  "mapbox-gl": {
104
104
  "src": "../../../../node_modules/.pnpm/mapbox-gl@3.17.0/node_modules/mapbox-gl/dist/mapbox-gl.js",
105
105
  "file": "mapbox-gl.js",
106
- "fileHash": "7f66cd45",
106
+ "fileHash": "f1176d3b",
107
107
  "needsInterop": true
108
108
  },
109
109
  "react-dom/client": {
110
110
  "src": "../../../../node_modules/.pnpm/react-dom@19.2.3_react@19.2.3/node_modules/react-dom/client.js",
111
111
  "file": "react-dom_client.js",
112
- "fileHash": "c9a3870a",
112
+ "fileHash": "e3385a5d",
113
113
  "needsInterop": true
114
114
  },
115
115
  "tailwind-merge": {
116
116
  "src": "../../../../node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.mjs",
117
117
  "file": "tailwind-merge.js",
118
- "fileHash": "9262acf9",
118
+ "fileHash": "8c12c67d",
119
119
  "needsInterop": false
120
120
  }
121
121
  },
@@ -1 +1 @@
1
- {"version":"4.0.16","results":[[":src/components/album/albums.test.tsx",{"duration":334.697216,"failed":false}],[":src/resources/review-resource.test.tsx",{"duration":555.3965230000001,"failed":false}],[":src/resources/carousel-resource.test.tsx",{"duration":255.80494199999998,"failed":false}],[":src/components/map/map-view.test.tsx",{"duration":80.3751279999999,"failed":false}],[":src/components/map/place-inspector.test.tsx",{"duration":425.41421500000024,"failed":false}],[":src/components/album/fullscreen-viewer.test.tsx",{"duration":254.88947699999972,"failed":false}],[":src/components/map/place-list.test.tsx",{"duration":128.85422500000004,"failed":false}],[":src/components/map/place-card.test.tsx",{"duration":346.91089099999976,"failed":false}],[":src/components/map/place-carousel.test.tsx",{"duration":450.42473600000017,"failed":false}],[":src/components/album/album-carousel.test.tsx",{"duration":95.884546,"failed":false}],[":src/components/carousel/carousel.test.tsx",{"duration":82.93981499999995,"failed":false}],[":src/resources/map-resource.test.tsx",{"duration":289.9950490000001,"failed":false}],[":src/resources/albums-resource.test.tsx",{"duration":250.440699,"failed":false}],[":src/components/album/film-strip.test.tsx",{"duration":397.1800680000001,"failed":false}],[":src/components/carousel/card.test.tsx",{"duration":74.95684000000006,"failed":false}],[":src/components/album/album-card.test.tsx",{"duration":283.6294740000001,"failed":false}]]}
1
+ {"version":"4.0.16","results":[[":src/resources/carousel-resource.test.tsx",{"duration":240.22361,"failed":false}],[":src/components/album/albums.test.tsx",{"duration":345.88829199999986,"failed":false}],[":src/resources/review-resource.test.tsx",{"duration":536.905929,"failed":false}],[":src/components/map/map-view.test.tsx",{"duration":56.95467699999995,"failed":false}],[":src/components/map/place-inspector.test.tsx",{"duration":419.31927799999994,"failed":false}],[":src/components/album/fullscreen-viewer.test.tsx",{"duration":230.5613269999999,"failed":false}],[":src/components/map/place-list.test.tsx",{"duration":137.19635000000017,"failed":false}],[":src/components/map/place-card.test.tsx",{"duration":369.79792799999996,"failed":false}],[":src/components/map/place-carousel.test.tsx",{"duration":451.7971869999999,"failed":false}],[":src/components/album/album-carousel.test.tsx",{"duration":108.27363300000002,"failed":false}],[":src/components/carousel/carousel.test.tsx",{"duration":88.2940570000003,"failed":false}],[":src/resources/map-resource.test.tsx",{"duration":267.9864110000001,"failed":false}],[":src/resources/albums-resource.test.tsx",{"duration":247.26545199999987,"failed":false}],[":src/components/album/album-card.test.tsx",{"duration":302.4231849999999,"failed":false}],[":src/components/album/film-strip.test.tsx",{"duration":447.1110639999997,"failed":false}],[":src/components/carousel/card.test.tsx",{"duration":79.15203599999995,"failed":false}]]}
@@ -7,7 +7,8 @@
7
7
  "dev": "sunpeak dev",
8
8
  "build": "sunpeak build",
9
9
  "mcp": "sunpeak mcp",
10
- "test": "vitest run"
10
+ "test": "vitest run",
11
+ "test:e2e": "playwright test"
11
12
  },
12
13
  "dependencies": {
13
14
  "@openai/apps-sdk-ui": "^0.2.1",
@@ -19,6 +20,7 @@
19
20
  "tailwind-merge": "^3.4.0"
20
21
  },
21
22
  "devDependencies": {
23
+ "@playwright/test": "^1.57.0",
22
24
  "@tailwindcss/vite": "^4.1.18",
23
25
  "@testing-library/jest-dom": "^6.9.1",
24
26
  "@testing-library/react": "^16.3.1",
@@ -0,0 +1,26 @@
1
+ import { defineConfig, devices } from '@playwright/test';
2
+
3
+ export default defineConfig({
4
+ testDir: './tests/e2e',
5
+ fullyParallel: true,
6
+ forbidOnly: !!process.env.CI,
7
+ retries: process.env.CI ? 2 : 0,
8
+ workers: process.env.CI ? 1 : undefined,
9
+ reporter: 'list',
10
+ use: {
11
+ baseURL: 'http://localhost:6776',
12
+ trace: 'on-first-retry',
13
+ },
14
+ projects: [
15
+ {
16
+ name: 'chromium',
17
+ use: { ...devices['Desktop Chrome'] },
18
+ },
19
+ ],
20
+ webServer: {
21
+ command: 'PORT=6776 pnpm dev',
22
+ url: 'http://localhost:6776',
23
+ reuseExistingServer: !process.env.CI,
24
+ timeout: 10000,
25
+ },
26
+ });
@@ -107,12 +107,12 @@ describe('ReviewResource', () => {
107
107
  expect(screen.getByText('Please review the following items')).toBeInTheDocument();
108
108
  });
109
109
 
110
- it('renders empty state when no sections', () => {
110
+ it('renders loading when no sections', () => {
111
111
  mockWidgetData = { title: 'Test', sections: [] };
112
112
 
113
113
  render(<ReviewResource />);
114
114
 
115
- expect(screen.getByText('Nothing to confirm')).toBeInTheDocument();
115
+ expect(screen.getByText('Loading...')).toBeInTheDocument();
116
116
  });
117
117
 
118
118
  it('has the correct displayName', () => {
@@ -425,7 +425,12 @@ export const ReviewResource = React.forwardRef<HTMLDivElement>((_props, ref) =>
425
425
 
426
426
  {/* Sections */}
427
427
  {sections.length === 0 ? (
428
- <p className="text-secondary text-center py-8">Nothing to confirm</p>
428
+ // Note: Apps cannot distinguish between "still loading" and "empty response".
429
+ // We show a loading state as the optimistic assumption.
430
+ <div className="flex items-center justify-center gap-2 py-8 text-secondary">
431
+ <div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
432
+ <span>Loading...</span>
433
+ </div>
429
434
  ) : (
430
435
  sections.map((section, i) => <Section key={i} section={section} />)
431
436
  )}
@@ -13,7 +13,7 @@
13
13
  "openai/resultCanProduceWidget": true
14
14
  }
15
15
  },
16
- "toolCall": {
16
+ "callToolResult": {
17
17
  "structuredContent": {
18
18
  "albums": [
19
19
  {
@@ -13,7 +13,7 @@
13
13
  "openai/resultCanProduceWidget": true
14
14
  }
15
15
  },
16
- "toolCall": {
16
+ "callToolResult": {
17
17
  "structuredContent": {
18
18
  "places": [
19
19
  {
@@ -13,7 +13,7 @@
13
13
  "openai/resultCanProduceWidget": true
14
14
  }
15
15
  },
16
- "toolCall": {
16
+ "callToolResult": {
17
17
  "structuredContent": {
18
18
  "places": [
19
19
  {
@@ -13,7 +13,7 @@
13
13
  "openai/resultCanProduceWidget": true
14
14
  }
15
15
  },
16
- "toolCall": {
16
+ "callToolResult": {
17
17
  "structuredContent": {
18
18
  "title": "Refactor Authentication Module",
19
19
  "description": "The following changes will update the authentication system to use JWT tokens instead of session-based auth.",
@@ -13,7 +13,7 @@
13
13
  "openai/resultCanProduceWidget": true
14
14
  }
15
15
  },
16
- "toolCall": {
16
+ "callToolResult": {
17
17
  "structuredContent": {
18
18
  "title": "Review Your Post",
19
19
  "description": "This post will be published to your connected accounts.",
@@ -13,7 +13,7 @@
13
13
  "openai/resultCanProduceWidget": true
14
14
  }
15
15
  },
16
- "toolCall": {
16
+ "callToolResult": {
17
17
  "structuredContent": {
18
18
  "title": "Confirm Your Order",
19
19
  "description": "Please review your order before completing the purchase.",
@@ -0,0 +1,4 @@
1
+ {
2
+ "status": "passed",
3
+ "failedTests": []
4
+ }
@@ -0,0 +1,120 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import { createSimulatorUrl } from 'sunpeak/chatgpt';
3
+
4
+ test.describe('Albums Resource', () => {
5
+ test.describe('Light Mode', () => {
6
+ test('should render album cards with correct styles', async ({ page }) => {
7
+ await page.goto(createSimulatorUrl({ simulation: 'albums-show', theme: 'light' }));
8
+
9
+ const albumCard = page.locator('button:has-text("Summer Slice")');
10
+ await expect(albumCard).toBeVisible();
11
+
12
+ // Verify album card unique styles
13
+ const styles = await albumCard.evaluate((el) => {
14
+ const computed = window.getComputedStyle(el);
15
+ return {
16
+ cursor: computed.cursor,
17
+ borderRadius: computed.borderRadius,
18
+ };
19
+ });
20
+
21
+ expect(styles.cursor).toBe('pointer');
22
+ expect(styles.borderRadius).toBe('12px'); // rounded-xl
23
+ });
24
+
25
+ test('should have album image with correct aspect ratio', async ({ page }) => {
26
+ await page.goto(createSimulatorUrl({ simulation: 'albums-show', theme: 'light' }));
27
+
28
+ const albumImage = page.locator('button:has-text("Summer Slice") img').first();
29
+ await expect(albumImage).toBeVisible();
30
+
31
+ // Verify aspect-[4/3] container
32
+ const imageContainer = page.locator('button:has-text("Summer Slice") .aspect-\\[4\\/3\\]');
33
+ await expect(imageContainer).toBeVisible();
34
+
35
+ const containerStyles = await imageContainer.evaluate((el) => {
36
+ const computed = window.getComputedStyle(el);
37
+ return {
38
+ borderRadius: computed.borderRadius,
39
+ overflow: computed.overflow,
40
+ };
41
+ });
42
+
43
+ expect(containerStyles.borderRadius).toBe('12px'); // rounded-xl
44
+ expect(containerStyles.overflow).toBe('hidden');
45
+ });
46
+ });
47
+
48
+ test.describe('Dark Mode', () => {
49
+ test('should render album cards with correct styles', async ({ page }) => {
50
+ await page.goto(createSimulatorUrl({ simulation: 'albums-show', theme: 'dark' }));
51
+
52
+ const albumCard = page.locator('button:has-text("Summer Slice")');
53
+ await expect(albumCard).toBeVisible();
54
+
55
+ const styles = await albumCard.evaluate((el) => {
56
+ const computed = window.getComputedStyle(el);
57
+ return {
58
+ cursor: computed.cursor,
59
+ borderRadius: computed.borderRadius,
60
+ };
61
+ });
62
+
63
+ expect(styles.cursor).toBe('pointer');
64
+ expect(styles.borderRadius).toBe('12px'); // rounded-xl
65
+ });
66
+
67
+ test('should have text with appropriate contrast', async ({ page }) => {
68
+ await page.goto(createSimulatorUrl({ simulation: 'albums-show', theme: 'dark' }));
69
+
70
+ const albumTitle = page.locator('button:has-text("Summer Slice") .text-primary').first();
71
+ await expect(albumTitle).toBeVisible();
72
+
73
+ // In dark mode, text-primary should be light colored for contrast
74
+ const titleStyles = await albumTitle.evaluate((el) => {
75
+ const computed = window.getComputedStyle(el);
76
+ return {
77
+ color: computed.color,
78
+ };
79
+ });
80
+
81
+ // Verify the text color exists (should be a light color in dark mode)
82
+ expect(titleStyles.color).toBeTruthy();
83
+ });
84
+ });
85
+
86
+ test.describe('Fullscreen Mode', () => {
87
+ test('should render correctly in fullscreen displayMode', async ({ page }) => {
88
+ await page.goto(
89
+ createSimulatorUrl({ simulation: 'albums-show', theme: 'light', displayMode: 'fullscreen' })
90
+ );
91
+
92
+ // Wait for content to load
93
+ await page.waitForLoadState('networkidle');
94
+
95
+ // The root container should be present
96
+ const root = page.locator('#root');
97
+ await expect(root).not.toBeEmpty();
98
+ });
99
+
100
+ test('should maintain album card styles in fullscreen', async ({ page }) => {
101
+ await page.goto(
102
+ createSimulatorUrl({ simulation: 'albums-show', theme: 'dark', displayMode: 'fullscreen' })
103
+ );
104
+
105
+ const albumCard = page.locator('button:has-text("Summer Slice")');
106
+ await expect(albumCard).toBeVisible();
107
+
108
+ const styles = await albumCard.evaluate((el) => {
109
+ const computed = window.getComputedStyle(el);
110
+ return {
111
+ cursor: computed.cursor,
112
+ borderRadius: computed.borderRadius,
113
+ };
114
+ });
115
+
116
+ expect(styles.cursor).toBe('pointer');
117
+ expect(styles.borderRadius).toBe('12px');
118
+ });
119
+ });
120
+ });
@@ -0,0 +1,127 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import { createSimulatorUrl } from 'sunpeak/chatgpt';
3
+
4
+ test.describe('Carousel Resource', () => {
5
+ test.describe('Light Mode', () => {
6
+ test('should render carousel cards with correct styles', async ({ page }) => {
7
+ await page.goto(createSimulatorUrl({ simulation: 'carousel-show', theme: 'light' }));
8
+
9
+ // Wait for carousel to load
10
+ await page.waitForLoadState('networkidle');
11
+
12
+ // Find a card in the carousel
13
+ const card = page.locator('.rounded-2xl').first();
14
+ await expect(card).toBeVisible();
15
+
16
+ const styles = await card.evaluate((el) => {
17
+ const computed = window.getComputedStyle(el);
18
+ return {
19
+ borderRadius: computed.borderRadius,
20
+ cursor: computed.cursor,
21
+ };
22
+ });
23
+
24
+ expect(styles.borderRadius).toBe('16px'); // rounded-2xl
25
+ expect(styles.cursor).toBe('pointer');
26
+ });
27
+
28
+ test('should have card with border styling', async ({ page }) => {
29
+ await page.goto(createSimulatorUrl({ simulation: 'carousel-show', theme: 'light' }));
30
+
31
+ await page.waitForLoadState('networkidle');
32
+
33
+ // Cards have border-subtle styling
34
+ const card = page.locator('.rounded-2xl.border').first();
35
+ await expect(card).toBeVisible();
36
+
37
+ const styles = await card.evaluate((el) => {
38
+ const computed = window.getComputedStyle(el);
39
+ return {
40
+ borderWidth: computed.borderWidth,
41
+ borderStyle: computed.borderStyle,
42
+ };
43
+ });
44
+
45
+ expect(styles.borderWidth).toBe('1px');
46
+ expect(styles.borderStyle).toBe('solid');
47
+ });
48
+
49
+ test('should have interactive buttons', async ({ page }) => {
50
+ await page.goto(createSimulatorUrl({ simulation: 'carousel-show', theme: 'light' }));
51
+
52
+ await page.waitForLoadState('networkidle');
53
+
54
+ // Find the Visit button (primary button)
55
+ const visitButton = page.locator('button:has-text("Visit")').first();
56
+ await expect(visitButton).toBeAttached();
57
+
58
+ const styles = await visitButton.evaluate((el) => {
59
+ const computed = window.getComputedStyle(el);
60
+ return {
61
+ cursor: computed.cursor,
62
+ };
63
+ });
64
+
65
+ expect(styles.cursor).toBe('pointer');
66
+ });
67
+ });
68
+
69
+ test.describe('Dark Mode', () => {
70
+ test('should render carousel cards with correct styles', async ({ page }) => {
71
+ await page.goto(createSimulatorUrl({ simulation: 'carousel-show', theme: 'dark' }));
72
+
73
+ await page.waitForLoadState('networkidle');
74
+
75
+ const card = page.locator('.rounded-2xl').first();
76
+ await expect(card).toBeVisible();
77
+
78
+ const styles = await card.evaluate((el) => {
79
+ const computed = window.getComputedStyle(el);
80
+ return {
81
+ borderRadius: computed.borderRadius,
82
+ cursor: computed.cursor,
83
+ };
84
+ });
85
+
86
+ expect(styles.borderRadius).toBe('16px'); // rounded-2xl
87
+ expect(styles.cursor).toBe('pointer');
88
+ });
89
+
90
+ test('should have appropriate background color for dark mode', async ({ page }) => {
91
+ await page.goto(createSimulatorUrl({ simulation: 'carousel-show', theme: 'dark' }));
92
+
93
+ await page.waitForLoadState('networkidle');
94
+
95
+ // Verify the card has a background color set
96
+ const card = page.locator('.rounded-2xl.bg-surface').first();
97
+ await expect(card).toBeVisible();
98
+
99
+ const styles = await card.evaluate((el) => {
100
+ const computed = window.getComputedStyle(el);
101
+ return {
102
+ backgroundColor: computed.backgroundColor,
103
+ };
104
+ });
105
+
106
+ // Background color should be set (not transparent)
107
+ expect(styles.backgroundColor).toBeTruthy();
108
+ expect(styles.backgroundColor).not.toBe('rgba(0, 0, 0, 0)');
109
+ });
110
+
111
+ test('should load without console errors', async ({ page }) => {
112
+ const errors: string[] = [];
113
+ page.on('console', (msg) => {
114
+ if (msg.type() === 'error') {
115
+ errors.push(msg.text());
116
+ }
117
+ });
118
+
119
+ await page.goto(createSimulatorUrl({ simulation: 'carousel-show', theme: 'dark' }));
120
+ await page.waitForLoadState('networkidle');
121
+
122
+ expect(errors).toHaveLength(0);
123
+ });
124
+ });
125
+
126
+ // Note: No fullscreen test for carousel as per requirements
127
+ });