@@ -163,27 +163,29 @@ async function seedLongTerminalLine(page: Page): Promise<void> {
163163 ) ;
164164}
165165
166- async function longPressTerminalRows ( page : Page ) : Promise < void > {
166+ async function longPressTerminalRows ( page : Page , rowIndex = 1 ) : Promise < void > {
167167 const rows = page . locator ( ".mobile-sheet--terminal .xterm-rows" ) . first ( ) ;
168- const host = page . locator ( ".mobile-sheet--terminal .xterm-host" ) . first ( ) ;
169168 await expect ( rows ) . toBeVisible ( { timeout : 10000 } ) ;
170- await expect ( host ) . toBeVisible ( { timeout : 10000 } ) ;
171- const box = await rows . boundingBox ( ) ;
169+
170+ const targetRow = rows . locator ( ":scope > div" ) . nth ( rowIndex ) ;
171+ await expect ( targetRow ) . toBeVisible ( { timeout : 10000 } ) ;
172+
173+ const box = await targetRow . boundingBox ( ) ;
172174 expect ( box ) . toBeTruthy ( ) ;
173175 if ( ! box ) {
174- throw new Error ( "xterm rows bounding box missing" ) ;
176+ throw new Error ( "xterm row bounding box missing" ) ;
175177 }
176178
177179 const x = box . x + Math . min ( 48 , Math . max ( 16 , box . width / 4 ) ) ;
178- const y = box . y + Math . min ( 32 , Math . max ( 12 , box . height / 3 ) ) ;
180+ const y = box . y + Math . min ( 16 , Math . max ( 8 , box . height / 2 ) ) ;
179181
180- await host . evaluate (
182+ await targetRow . evaluate (
181183 ( node , { clientX, clientY } ) => {
182184 if ( ! ( node instanceof HTMLElement ) ) {
183- throw new Error ( "xterm host missing" ) ;
185+ throw new Error ( "xterm row missing" ) ;
184186 }
185187
186- const touches = [ { identifier : 1 , clientX, clientY } ] ;
188+ const touches = [ { identifier : 1 , clientX, clientY, target : node } ] ;
187189 const buildEvent = (
188190 type : string ,
189191 activeTouches : typeof touches ,
@@ -205,6 +207,13 @@ async function longPressTerminalRows(page: Page): Promise<void> {
205207 ) ;
206208}
207209
210+ async function findPrintedLineRowIndex ( page : Page ) : Promise < number > {
211+ const rowTexts = await page
212+ . locator ( ".mobile-sheet--terminal .xterm-rows > div" )
213+ . allTextContents ( ) ;
214+ return rowTexts . findIndex ( ( text ) => text . startsWith ( "MOBILE_COPY_MODE_LONG_LINE" ) ) ;
215+ }
216+
208217async function setMobileCopyOnSelect ( page : Page , enabled : boolean ) : Promise < void > {
209218 await page . goto ( "/settings" ) ;
210219 await expect ( page . locator ( ".settings-page" ) ) . toBeVisible ( { timeout : 15000 } ) ;
@@ -228,10 +237,37 @@ test.describe("mobile copy on select", () => {
228237 test . beforeEach ( async ( { page } ) => {
229238 await page . addInitScript ( ( ) => {
230239 window . localStorage . setItem ( "ui.locale" , JSON . stringify ( "en" ) ) ;
240+
241+ let copiedText = "" ;
242+ Object . defineProperty ( window , "__mobileCopiedText" , {
243+ configurable : true ,
244+ get ( ) {
245+ return copiedText ;
246+ } ,
247+ } ) ;
248+
249+ if ( navigator . clipboard ) {
250+ Object . defineProperty ( navigator . clipboard , "writeText" , {
251+ configurable : true ,
252+ value : async ( text : string ) => {
253+ copiedText = text ;
254+ } ,
255+ } ) ;
256+ return ;
257+ }
258+
259+ Object . defineProperty ( navigator , "clipboard" , {
260+ configurable : true ,
261+ value : {
262+ writeText : async ( text : string ) => {
263+ copiedText = text ;
264+ } ,
265+ } ,
266+ } ) ;
231267 } ) ;
232268 } ) ;
233269
234- test ( "mobile copy mode respects the setting and does not expand page or sheet layout " , async ( {
270+ test ( "mobile long press copies the wrapped logical line without opening a copy overlay " , async ( {
235271 page,
236272 } ) => {
237273 await setMobileCopyOnSelect ( page , true ) ;
@@ -240,119 +276,59 @@ test.describe("mobile copy on select", () => {
240276 await ensureTerminalExists ( page ) ;
241277 await seedLongTerminalLine ( page ) ;
242278
243- const beforeMetrics = await page . evaluate ( ( ) => {
244- const sheet = document . querySelector ( ".mobile-sheet--terminal" ) ;
245- const terminalSheet = document . querySelector ( ".mobile-terminal-sheet" ) ;
246- const body = document . body ;
247- const doc = document . documentElement ;
248- const viewportWidth = window . innerWidth ;
249- const viewportHeight = window . innerHeight ;
250-
251- return {
252- viewportWidth,
253- viewportHeight,
254- docScrollWidth : doc . scrollWidth ,
255- docScrollHeight : doc . scrollHeight ,
256- bodyScrollWidth : body . scrollWidth ,
257- bodyScrollHeight : body . scrollHeight ,
258- sheetRect : sheet ?. getBoundingClientRect ( ) . toJSON ( ) ?? null ,
259- terminalSheetRect : terminalSheet ?. getBoundingClientRect ( ) . toJSON ( ) ?? null ,
260- } ;
261- } ) ;
262-
263- await longPressTerminalRows ( page ) ;
264-
265- const copyMode = page . locator ( ".mobile-terminal-copy-mode" ) ;
266- await expect ( copyMode ) . toBeVisible ( { timeout : 5000 } ) ;
267- await expect (
268- copyMode . getByText ( translateForE2E ( "terminal.copy_mode_title" , "en" ) )
269- ) . toBeVisible ( ) ;
270-
271- const afterMetrics = await page . evaluate ( ( ) => {
272- const body = document . body ;
273- const doc = document . documentElement ;
274- const overlay = document . querySelector ( ".mobile-terminal-copy-mode" ) ;
275- const toolbar = document . querySelector ( ".mobile-terminal-copy-mode__toolbar" ) ;
276- const content = document . querySelector ( ".mobile-terminal-copy-mode__content" ) ;
277- const text = document . querySelector ( ".mobile-terminal-copy-mode__text" ) ;
278- const sheet = document . querySelector ( ".mobile-sheet--terminal" ) ;
279- const terminalSheet = document . querySelector ( ".mobile-terminal-sheet" ) ;
280- const xtermViewport = document . querySelector ( ".mobile-sheet--terminal .xterm-viewport" ) ;
281- const xtermRows = document . querySelector ( ".mobile-sheet--terminal .xterm-rows" ) ;
282- const viewportWidth = window . innerWidth ;
283- const viewportHeight = window . innerHeight ;
284-
285- const rect = ( node : Element | null ) => node ?. getBoundingClientRect ( ) . toJSON ( ) ?? null ;
286-
287- return {
288- viewportWidth,
289- viewportHeight,
290- docScrollWidth : doc . scrollWidth ,
291- docScrollHeight : doc . scrollHeight ,
292- bodyScrollWidth : body . scrollWidth ,
293- bodyScrollHeight : body . scrollHeight ,
294- overlayRect : rect ( overlay ) ,
295- toolbarRect : rect ( toolbar ) ,
296- contentRect : rect ( content ) ,
297- textRect : rect ( text ) ,
298- sheetRect : rect ( sheet ) ,
299- terminalSheetRect : rect ( terminalSheet ) ,
300- xtermViewportRect : rect ( xtermViewport ) ,
301- xtermRowsRect : rect ( xtermRows ) ,
302- contentClientHeight : content instanceof HTMLElement ? content . clientHeight : null ,
303- contentScrollHeight : content instanceof HTMLElement ? content . scrollHeight : null ,
304- textClientHeight : text instanceof HTMLElement ? text . clientHeight : null ,
305- textScrollHeight : text instanceof HTMLElement ? text . scrollHeight : null ,
306- } ;
307- } ) ;
279+ await expect
280+ . poll ( async ( ) => {
281+ return page . locator ( ".mobile-sheet--terminal .xterm-rows > div" ) . count ( ) ;
282+ } )
283+ . toBeGreaterThan ( 1 ) ;
308284
309- expect ( afterMetrics . docScrollWidth ) . toBeLessThanOrEqual ( afterMetrics . viewportWidth ) ;
310- expect ( afterMetrics . bodyScrollWidth ) . toBeLessThanOrEqual ( afterMetrics . viewportWidth ) ;
311- expect ( afterMetrics . docScrollHeight ) . toBeLessThanOrEqual ( afterMetrics . viewportHeight ) ;
312- expect ( afterMetrics . bodyScrollHeight ) . toBeLessThanOrEqual ( afterMetrics . viewportHeight ) ;
285+ await expect
286+ . poll ( async ( ) => {
287+ return findPrintedLineRowIndex ( page ) ;
288+ } )
289+ . not . toBe ( - 1 ) ;
290+ const printedLineRowIndex = await findPrintedLineRowIndex ( page ) ;
291+ await longPressTerminalRows ( page , printedLineRowIndex ) ;
313292
314- expect ( afterMetrics . overlayRect ?. width ?? 0 ) . toBeLessThanOrEqual ( afterMetrics . viewportWidth ) ;
315- expect ( afterMetrics . overlayRect ?. height ?? 0 ) . toBeLessThanOrEqual ( afterMetrics . viewportHeight ) ;
316- expect ( afterMetrics . contentRect ?. width ?? 0 ) . toBeLessThanOrEqual (
317- ( afterMetrics . overlayRect ?. width ?? afterMetrics . viewportWidth ) + 1
293+ await expect ( page . getByText ( translateForE2E ( "terminal.copied_current_line" , "en" ) ) ) . toBeVisible (
294+ { timeout : 5000 }
318295 ) ;
319- expect ( afterMetrics . sheetRect ?. width ?? 0 ) . toBeLessThanOrEqual ( afterMetrics . viewportWidth ) ;
320- expect ( afterMetrics . sheetRect ?. height ?? 0 ) . toBeLessThanOrEqual ( afterMetrics . viewportHeight ) ;
321-
322- expect (
323- Math . abs ( ( afterMetrics . sheetRect ?. width ?? 0 ) - ( beforeMetrics . sheetRect ?. width ?? 0 ) )
324- ) . toBeLessThanOrEqual ( 1 ) ;
325- expect (
326- Math . abs ( ( afterMetrics . sheetRect ?. height ?? 0 ) - ( beforeMetrics . sheetRect ?. height ?? 0 ) )
327- ) . toBeLessThanOrEqual ( 1 ) ;
328- expect (
329- Math . abs (
330- ( afterMetrics . terminalSheetRect ?. width ?? 0 ) - ( beforeMetrics . terminalSheetRect ?. width ?? 0 )
331- )
332- ) . toBeLessThanOrEqual ( 1 ) ;
333- expect (
334- Math . abs (
335- ( afterMetrics . terminalSheetRect ?. height ?? 0 ) -
336- ( beforeMetrics . terminalSheetRect ?. height ?? 0 )
337- )
338- ) . toBeLessThanOrEqual ( 1 ) ;
339- expect (
340- afterMetrics . contentScrollHeight ,
341- `copy mode content should not overflow by default:\n${ JSON . stringify ( afterMetrics , null , 2 ) } `
342- ) . toBeLessThanOrEqual ( ( afterMetrics . contentClientHeight ?? 0 ) + 1 ) ;
296+ await expect ( page . locator ( ".mobile-terminal-copy-mode" ) ) . toHaveCount ( 0 ) ;
297+
298+ await expect
299+ . poll ( async ( ) => {
300+ return page . evaluate ( ( ) => {
301+ return ( window as Window & { __mobileCopiedText ?: string } ) . __mobileCopiedText ?? "" ;
302+ } ) ;
303+ } )
304+ . toBe ( LONG_LINE_TEXT ) ;
343305 } ) ;
344306
345- test ( "mobile long press does not enter copy mode when copy on select is disabled" , async ( {
346- page,
347- } ) => {
307+ test ( "mobile long press does not copy when copy on select is disabled" , async ( { page } ) => {
348308 await setMobileCopyOnSelect ( page , false ) ;
349309 await openMobileWorkspace ( page ) ;
350310 await openMobileTerminalSheet ( page ) ;
351311 await ensureTerminalExists ( page ) ;
352312 await seedLongTerminalLine ( page ) ;
353313
354- await longPressTerminalRows ( page ) ;
314+ await expect
315+ . poll ( async ( ) => {
316+ return findPrintedLineRowIndex ( page ) ;
317+ } )
318+ . not . toBe ( - 1 ) ;
319+ const printedLineRowIndex = await findPrintedLineRowIndex ( page ) ;
320+ await longPressTerminalRows ( page , printedLineRowIndex ) ;
355321
356322 await expect ( page . locator ( ".mobile-terminal-copy-mode" ) ) . toHaveCount ( 0 ) ;
323+ await expect ( page . getByText ( translateForE2E ( "terminal.copied_current_line" , "en" ) ) ) . toHaveCount (
324+ 0
325+ ) ;
326+ await expect
327+ . poll ( async ( ) => {
328+ return page . evaluate ( ( ) => {
329+ return ( window as Window & { __mobileCopiedText ?: string } ) . __mobileCopiedText ?? "" ;
330+ } ) ;
331+ } )
332+ . toBe ( "" ) ;
357333 } ) ;
358334} ) ;
0 commit comments