Skip to content

Commit eda441d

Browse files
committed
Create a slight delay in on shortcut keys so you can't spam them and open multiple tabs.
1 parent 06d8de3 commit eda441d

File tree

3 files changed

+169
-20
lines changed

3 files changed

+169
-20
lines changed

.changeset/ninety-shirts-guess.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/theme': patch
3+
---
4+
5+
Create a slight delay when keypressing theme dev shortcut keys to stop accidental copy pasting and opening up a ton of tabs

packages/theme/src/cli/services/dev.test.ts

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import {openURLSafely, renderLinks} from './dev.js'
2-
import {describe, expect, test, vi} from 'vitest'
1+
import {openURLSafely, renderLinks, createKeypressHandler} from './dev.js'
2+
import {describe, expect, test, vi, beforeEach, afterEach} from 'vitest'
33
import {buildTheme} from '@shopify/cli-kit/node/themes/factories'
44
import {DEVELOPMENT_THEME_ROLE} from '@shopify/cli-kit/node/themes/utils'
55
import {renderSuccess, renderWarning} from '@shopify/cli-kit/node/ui'
@@ -102,3 +102,134 @@ describe('openURLSafely', () => {
102102
})
103103
})
104104
})
105+
106+
describe('createKeypressHandler', () => {
107+
const urls = {
108+
local: 'http://127.0.0.1:9292',
109+
giftCard: 'http://127.0.0.1:9292/gift_cards/[store_id]/preview',
110+
themeEditor: 'https://my-store.myshopify.com/admin/themes/123/editor?hr=9292',
111+
preview: 'https://my-store.myshopify.com/?preview_theme_id=123',
112+
}
113+
114+
const ctx = {lastRequestedPath: '/'}
115+
116+
beforeEach(() => {
117+
vi.mocked(openURL).mockResolvedValue(true)
118+
vi.useFakeTimers()
119+
})
120+
121+
afterEach(() => {
122+
vi.useRealTimers()
123+
})
124+
125+
test('opens localhost when "t" is pressed', () => {
126+
// Given
127+
const handler = createKeypressHandler(urls, ctx)
128+
129+
// When
130+
handler('t', {name: 't'})
131+
132+
// Then
133+
expect(openURL).toHaveBeenCalledWith(urls.local)
134+
})
135+
136+
test('opens theme preview when "p" is pressed', () => {
137+
// Given
138+
const handler = createKeypressHandler(urls, ctx)
139+
140+
// When
141+
handler('p', {name: 'p'})
142+
143+
// Then
144+
expect(openURL).toHaveBeenCalledWith(urls.preview)
145+
})
146+
147+
test('opens theme editor when "e" is pressed', () => {
148+
// Given
149+
const handler = createKeypressHandler(urls, ctx)
150+
151+
// When
152+
handler('e', {name: 'e'})
153+
154+
// Then
155+
expect(openURL).toHaveBeenCalledWith(urls.themeEditor)
156+
})
157+
158+
test('opens gift card preview when "g" is pressed', () => {
159+
// Given
160+
const handler = createKeypressHandler(urls, ctx)
161+
162+
// When
163+
handler('g', {name: 'g'})
164+
165+
// Then
166+
expect(openURL).toHaveBeenCalledWith(urls.giftCard)
167+
})
168+
169+
test('appends preview path to theme editor URL when lastRequestedPath is not "/"', () => {
170+
// Given
171+
const ctxWithPath = {lastRequestedPath: '/products/test-product'}
172+
const handler = createKeypressHandler(urls, ctxWithPath)
173+
174+
// When
175+
handler('e', {name: 'e'})
176+
177+
// Then
178+
expect(openURL).toHaveBeenCalledWith(
179+
`${urls.themeEditor}&previewPath=${encodeURIComponent('/products/test-product')}`,
180+
)
181+
})
182+
183+
test('debounces rapid keypresses - only opens URL once during debounce window', () => {
184+
// Given
185+
const handler = createKeypressHandler(urls, ctx)
186+
187+
// When
188+
handler('t', {name: 't'})
189+
handler('t', {name: 't'})
190+
handler('t', {name: 't'})
191+
handler('t', {name: 't'})
192+
193+
// Then
194+
expect(openURL).toHaveBeenCalledTimes(1)
195+
expect(openURL).toHaveBeenCalledWith(urls.local)
196+
})
197+
198+
test('allows keypresses after debounce period expires', () => {
199+
// Given
200+
const handler = createKeypressHandler(urls, ctx)
201+
202+
// When
203+
handler('t', {name: 't'})
204+
expect(openURL).toHaveBeenCalledTimes(1)
205+
206+
handler('t', {name: 't'})
207+
handler('t', {name: 't'})
208+
expect(openURL).toHaveBeenCalledTimes(1)
209+
210+
// Advance time to exceed debounce period
211+
vi.advanceTimersByTime(100)
212+
213+
handler('p', {name: 'p'})
214+
215+
// Then
216+
expect(openURL).toHaveBeenCalledTimes(2)
217+
expect(openURL).toHaveBeenNthCalledWith(1, urls.local)
218+
expect(openURL).toHaveBeenNthCalledWith(2, urls.preview)
219+
})
220+
221+
test('debounces different keys during the same debounce window', () => {
222+
// Given
223+
const handler = createKeypressHandler(urls, ctx)
224+
225+
// When
226+
handler('t', {name: 't'})
227+
handler('p', {name: 'p'})
228+
handler('e', {name: 'e'})
229+
handler('g', {name: 'g'})
230+
231+
// Then
232+
expect(openURL).toHaveBeenCalledTimes(1)
233+
expect(openURL).toHaveBeenCalledWith(urls.local)
234+
})
235+
})

packages/theme/src/cli/services/dev.ts

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {Theme} from '@shopify/cli-kit/node/themes/types'
1212
import {checkPortAvailability, getAvailableTCPPort} from '@shopify/cli-kit/node/tcp'
1313
import {AbortError} from '@shopify/cli-kit/node/error'
1414
import {openURL} from '@shopify/cli-kit/node/system'
15+
import {debounce} from '@shopify/cli-kit/common/function'
1516
import chalk from '@shopify/cli-kit/node/colors'
1617
import readline from 'readline'
1718

@@ -126,43 +127,55 @@ export async function dev(options: DevOptions) {
126127
process.stdin.setRawMode(true)
127128
}
128129

129-
process.stdin.on('keypress', (_str, key) => {
130+
const keypressHandler = createKeypressHandler(urls, ctx)
131+
process.stdin.on('keypress', keypressHandler)
132+
133+
await Promise.all([
134+
backgroundJobPromise,
135+
renderDevSetupProgress()
136+
.then(serverStart)
137+
.then(() => {
138+
renderLinks(urls)
139+
if (options.open) {
140+
openURLSafely(urls.local, 'development server')
141+
}
142+
}),
143+
])
144+
}
145+
146+
export function createKeypressHandler(
147+
urls: {local: string; giftCard: string; themeEditor: string; preview: string},
148+
ctx: {lastRequestedPath: string},
149+
) {
150+
const debouncedOpenURL = debounce(openURLSafely, 100, {leading: true, trailing: false})
151+
152+
return (_str: string, key: {ctrl?: boolean; name?: string}) => {
130153
if (key.ctrl && key.name === 'c') {
131154
process.exit()
132155
}
133156

134157
switch (key.name) {
135158
case 't':
136-
openURLSafely(urls.local, 'localhost')
159+
debouncedOpenURL(urls.local, 'localhost')
137160
break
138161
case 'p':
139-
openURLSafely(urls.preview, 'theme preview')
162+
debouncedOpenURL(urls.preview, 'theme preview')
140163
break
141164
case 'e':
142-
openURLSafely(
165+
debouncedOpenURL(
143166
ctx.lastRequestedPath === '/'
144167
? urls.themeEditor
145168
: `${urls.themeEditor}&previewPath=${encodeURIComponent(ctx.lastRequestedPath)}`,
146169
'theme editor',
147170
)
148171
break
149172
case 'g':
150-
openURLSafely(urls.giftCard, 'gift card preview')
173+
debouncedOpenURL(urls.giftCard, 'gift card preview')
174+
break
175+
default:
151176
break
152177
}
153-
})
154-
155-
await Promise.all([
156-
backgroundJobPromise,
157-
renderDevSetupProgress()
158-
.then(serverStart)
159-
.then(() => {
160-
renderLinks(urls)
161-
if (options.open) {
162-
openURLSafely(urls.local, 'development server')
163-
}
164-
}),
165-
])
178+
}
166179
}
167180

168181
export function openURLSafely(url: string, label: string) {

0 commit comments

Comments
 (0)