Skip to content

Commit 5787117

Browse files
committed
wip
1 parent 6047afe commit 5787117

14 files changed

Lines changed: 888 additions & 0 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Theme E2E Test Auth Refactoring
2+
3+
## Summary
4+
5+
Refactored theme E2E tests to reuse the existing `authFixture` (OAuth-based) instead of the separate password-based `themeAuthFixture`. This simplifies authentication by using OAuth which already includes theme scopes.
6+
7+
## Steps Completed
8+
9+
| Step | Description | Status |
10+
|------|-------------|--------|
11+
| 1 | Update `packages/e2e/setup/theme.ts` to extend from `authFixture` | DONE |
12+
| 2 | Update test files to use `requireEnv` instead of `requireThemeEnv` | DONE |
13+
| 3 | Delete `packages/e2e/setup/theme-auth.ts` | DONE |
14+
| 4 | Clean up `packages/e2e/setup/env.ts` | DONE |
15+
| 5 | Update `.github/workflows/tests-pr.yml` | DONE |
16+
| 6 | Update `packages/e2e/.env.example` | DONE |
17+
18+
## Files Changed
19+
20+
### Modified
21+
- `packages/e2e/setup/theme.ts` - Changed to extend `authFixture` instead of `themeAuthFixture`
22+
- `packages/e2e/setup/env.ts` - Removed `themeToken` from interface and `requireThemeEnv()` function
23+
- `packages/e2e/tests/theme-crud.spec.ts` - Changed to use `requireEnv(env, 'storeFqdn')`
24+
- `packages/e2e/tests/theme-dev.spec.ts` - Changed to use `requireEnv(env, 'storeFqdn')`
25+
- `packages/e2e/tests/theme-console.spec.ts` - Changed to use `requireEnv(env, 'storeFqdn')`
26+
- `.github/workflows/tests-pr.yml` - Removed `SHOPIFY_CLI_THEME_TOKEN` env var
27+
- `packages/e2e/.env.example` - Removed theme token documentation
28+
29+
### Deleted
30+
- `packages/e2e/setup/theme-auth.ts` - No longer needed
31+
32+
## Verification
33+
34+
- `pnpm nx run e2e:lint --skip-nx-cache` - PASS (only pre-existing warning)
35+
- `pnpm nx run e2e:type-check --skip-nx-cache` - PASS
36+
37+
## Deviations
38+
39+
| Deviation | Reason | Escalated? |
40+
|-----------|--------|------------|
41+
| NONE | - | - |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
body { font-family: sans-serif; }
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[{"name": "theme_info", "theme_name": "E2E Test Theme", "theme_version": "1.0.0"}]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>{{ content_for_header }}</head>
4+
<body>{{ content_for_layout }}</body>
5+
</html>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"general": {"title": "E2E Test Store"}}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<header>E2E Test Header</header>
2+
{% schema %}{"name": "Header"}{% endschema %}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<span class="icon">{{ icon }}</span>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"sections": {"main": {"type": "header"}}, "order": ["main"]}

packages/e2e/setup/theme.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/* eslint-disable no-restricted-imports */
2+
import {authFixture} from './auth.js'
3+
import * as path from 'path'
4+
import * as fs from 'fs'
5+
import {fileURLToPath} from 'url'
6+
import type {ExecResult} from './cli.js'
7+
8+
const __filename = fileURLToPath(import.meta.url)
9+
const __dirname = path.dirname(__filename)
10+
11+
const FIXTURE_DIR = path.join(__dirname, '../data/dawn-minimal')
12+
13+
export interface ThemeScaffold {
14+
/** The directory containing the theme files */
15+
themeDir: string
16+
/** Push theme to store, returns theme ID from output */
17+
push(opts?: {unpublished?: boolean; themeName?: string}): Promise<{result: ExecResult; themeId?: string}>
18+
/** Pull theme from store by ID */
19+
pull(themeId: string): Promise<ExecResult>
20+
/** List all themes on the store */
21+
list(): Promise<{result: ExecResult; themes: {id: string; name: string; role: string}[]}>
22+
/** Delete a theme by ID */
23+
delete(themeId: string): Promise<ExecResult>
24+
/** Rename a theme */
25+
rename(themeId: string, newName: string): Promise<ExecResult>
26+
/** Duplicate a theme (via push with development flag) */
27+
duplicate(themeId: string, newName: string): Promise<ExecResult>
28+
}
29+
30+
/**
31+
* Recursively copies a directory.
32+
*/
33+
function copyDirRecursive(src: string, dest: string): void {
34+
fs.mkdirSync(dest, {recursive: true})
35+
for (const entry of fs.readdirSync(src, {withFileTypes: true})) {
36+
const srcPath = path.join(src, entry.name)
37+
const destPath = path.join(dest, entry.name)
38+
if (entry.isDirectory()) {
39+
copyDirRecursive(srcPath, destPath)
40+
} else {
41+
fs.copyFileSync(srcPath, destPath)
42+
}
43+
}
44+
}
45+
46+
/**
47+
* Test-scoped fixture that copies the dawn-minimal fixture to a temp directory.
48+
* Provides helper methods for theme CRUD operations.
49+
* Depends on authLogin (worker-scoped) for OAuth session.
50+
*/
51+
export const themeScaffoldFixture = authFixture.extend<{themeScaffold: ThemeScaffold}>({
52+
themeScaffold: async ({cli, env, authLogin: _authLogin}, use) => {
53+
const themeDir = fs.mkdtempSync(path.join(env.tempDir, 'theme-'))
54+
const createdThemeIds: string[] = []
55+
const storeFqdn = env.storeFqdn
56+
57+
// Copy fixture files recursively
58+
copyDirRecursive(FIXTURE_DIR, themeDir)
59+
60+
const scaffold: ThemeScaffold = {
61+
themeDir,
62+
63+
async push(opts = {}) {
64+
const themeName = opts.themeName ?? `e2e-test-${Date.now()}`
65+
const args = ['theme', 'push', '--store', storeFqdn, '--path', themeDir, '--theme', themeName]
66+
if (opts.unpublished !== false) {
67+
args.push('--unpublished')
68+
}
69+
// Add --json for parseable output
70+
args.push('--json')
71+
72+
const result = await cli.exec(args, {timeout: 2 * 60 * 1000})
73+
74+
// Try to extract theme ID from JSON output
75+
let themeId: string | undefined
76+
try {
77+
const json = JSON.parse(result.stdout)
78+
if (json.theme?.id) {
79+
themeId = String(json.theme.id)
80+
createdThemeIds.push(themeId)
81+
}
82+
} catch (error) {
83+
// JSON parsing failed, try regex fallback
84+
if (!(error instanceof SyntaxError)) throw error
85+
const match = result.stdout.match(/theme[:\s]+(\d+)/i) ?? result.stderr.match(/theme[:\s]+(\d+)/i)
86+
if (match?.[1]) {
87+
themeId = match[1]
88+
createdThemeIds.push(themeId)
89+
}
90+
}
91+
92+
return {result, themeId}
93+
},
94+
95+
async pull(themeId: string) {
96+
return cli.exec(['theme', 'pull', '--store', storeFqdn, '--path', themeDir, '--theme', themeId], {
97+
timeout: 2 * 60 * 1000,
98+
})
99+
},
100+
101+
async list() {
102+
const result = await cli.exec(['theme', 'list', '--store', storeFqdn, '--json'], {timeout: 60 * 1000})
103+
const themes: {id: string; name: string; role: string}[] = []
104+
105+
try {
106+
const json = JSON.parse(result.stdout)
107+
if (Array.isArray(json)) {
108+
for (const theme of json) {
109+
themes.push({
110+
id: String(theme.id),
111+
name: theme.name ?? '',
112+
role: theme.role ?? '',
113+
})
114+
}
115+
}
116+
} catch (error) {
117+
// JSON parsing failed - return empty array
118+
if (!(error instanceof SyntaxError)) throw error
119+
}
120+
121+
return {result, themes}
122+
},
123+
124+
async delete(themeId: string) {
125+
const result = await cli.exec(['theme', 'delete', '--store', storeFqdn, '--theme', themeId, '--force'], {
126+
timeout: 60 * 1000,
127+
})
128+
// Remove from tracked IDs if successful
129+
const idx = createdThemeIds.indexOf(themeId)
130+
if (idx >= 0 && result.exitCode === 0) {
131+
createdThemeIds.splice(idx, 1)
132+
}
133+
return result
134+
},
135+
136+
async rename(themeId: string, newName: string) {
137+
return cli.exec(['theme', 'rename', '--store', storeFqdn, '--theme', themeId, '--name', newName], {
138+
timeout: 60 * 1000,
139+
})
140+
},
141+
142+
async duplicate(themeId: string, newName: string) {
143+
// Pull the theme first, then push with new name
144+
const pullResult = await this.pull(themeId)
145+
if (pullResult.exitCode !== 0) {
146+
return pullResult
147+
}
148+
const {result} = await this.push({themeName: newName})
149+
return result
150+
},
151+
}
152+
153+
await use(scaffold)
154+
155+
// Teardown: delete all themes created during the test (parallel for speed)
156+
await Promise.all(
157+
createdThemeIds.map((themeId) =>
158+
cli
159+
.exec(['theme', 'delete', '--store', storeFqdn, '--theme', themeId, '--force'], {timeout: 60 * 1000})
160+
.catch(() => {
161+
// Best effort cleanup - don't fail teardown
162+
}),
163+
),
164+
)
165+
166+
// Cleanup temp directory
167+
fs.rmSync(themeDir, {recursive: true, force: true})
168+
},
169+
})
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import {themeScaffoldFixture as test} from '../setup/theme.js'
2+
import {requireEnv} from '../setup/env.js'
3+
import {expect} from '@playwright/test'
4+
5+
test.describe('Theme console', () => {
6+
test('console evaluates Liquid expressions', async ({themeScaffold, cli, env}) => {
7+
requireEnv(env, 'storeFqdn')
8+
9+
// Step 1: Push a theme first so we have something to work with
10+
const themeName = `e2e-test-console-${Date.now()}`
11+
const {result: pushResult, themeId} = await themeScaffold.push({themeName, unpublished: true})
12+
expect(pushResult.exitCode).toBe(0)
13+
expect(themeId).toBeDefined()
14+
15+
// Step 2: Start console via PTY
16+
// Unset CI so the REPL is interactive
17+
const console = await cli.spawn(
18+
['theme', 'console', '--store', env.storeFqdn, '--url', `https://${env.storeFqdn}`],
19+
{
20+
env: {CI: ''},
21+
},
22+
)
23+
24+
// Step 3: Wait for the console to be ready
25+
// Theme console outputs "Welcome to Shopify Liquid console" when ready
26+
await console.waitForOutput('Welcome to Shopify Liquid console', 60_000)
27+
28+
// Step 4: Send a Liquid expression (without {{ }} delimiters - console doesn't support them)
29+
console.sendLine('1 | plus: 2')
30+
31+
// Step 5: Wait for the result
32+
await console.waitForOutput('3', 30_000)
33+
34+
// Step 6: Verify the result is in the output
35+
const output = console.getOutput()
36+
expect(output).toContain('3')
37+
38+
// Step 7: Exit the console
39+
// Send Ctrl+C or type 'exit'
40+
// Ctrl+C
41+
console.sendKey('\x03')
42+
43+
// Step 8: Wait for exit (may timeout if Ctrl+C doesn't work, that's OK)
44+
try {
45+
await console.waitForExit(10_000)
46+
// eslint-disable-next-line no-catch-all/no-catch-all
47+
} catch (_error) {
48+
// Timeout errors are expected - force kill if it doesn't exit gracefully
49+
console.kill()
50+
}
51+
52+
// Cleanup
53+
await themeScaffold.delete(themeId!)
54+
})
55+
56+
test('console with --url evaluates in product context', async ({themeScaffold, cli, env}) => {
57+
requireEnv(env, 'storeFqdn')
58+
59+
// Step 1: Push a theme first so we have something to work with
60+
const themeName = `e2e-test-console-url-${Date.now()}`
61+
const {result: pushResult, themeId} = await themeScaffold.push({themeName, unpublished: true})
62+
expect(pushResult.exitCode).toBe(0)
63+
expect(themeId).toBeDefined()
64+
65+
// Step 2: Start console via PTY with --url pointing to products page
66+
// Using /products as a generic URL that should work on any store
67+
// Unset CI so the REPL is interactive
68+
const consoleProc = await cli.spawn(
69+
['theme', 'console', '--store', env.storeFqdn, '--url', `https://${env.storeFqdn}/products`],
70+
{
71+
env: {CI: ''},
72+
},
73+
)
74+
75+
// Step 3: Wait for the console to be ready
76+
// Theme console outputs "Welcome to Shopify Liquid console" when ready
77+
await consoleProc.waitForOutput('Welcome to Shopify Liquid console', 60_000)
78+
79+
// Step 4: Try to evaluate something that would exist on a products page
80+
// Even if no products exist, the template context should be set
81+
// Note: console doesn't support {{ }} delimiters
82+
consoleProc.sendLine('request.path')
83+
84+
// Step 5: Wait for output - should show /products or similar
85+
await consoleProc.waitForOutput('products', 30_000)
86+
87+
// Step 6: Exit the console (Ctrl+C)
88+
consoleProc.sendKey('\x03')
89+
90+
// Step 7: Wait for exit
91+
try {
92+
await consoleProc.waitForExit(10_000)
93+
// eslint-disable-next-line no-catch-all/no-catch-all
94+
} catch (_error) {
95+
// Timeout errors are expected - force kill if it doesn't exit gracefully
96+
consoleProc.kill()
97+
}
98+
99+
// Cleanup
100+
await themeScaffold.delete(themeId!)
101+
})
102+
})

0 commit comments

Comments
 (0)