@@ -198,6 +198,82 @@ test.describe('Browser Recovery Tool', () => {
198198 await recovery . expectNeedMoreShares ( 1 ) ;
199199 } ) ;
200200
201+ test ( 'recover via typed words in paste area' , async ( { page } ) => {
202+ const [ aliceDir , bobDir ] = extractBundles ( bundlesDir , [ 'Alice' , 'Bob' ] ) ;
203+ const recovery = new RecoveryPage ( page , aliceDir ) ;
204+
205+ await recovery . open ( ) ;
206+
207+ // Alice's share is pre-loaded via personalization
208+ await recovery . expectShareCount ( 1 ) ;
209+
210+ // Read Bob's README.txt to extract the share words
211+ const bobReadme = fs . readFileSync ( path . join ( bobDir , 'README.txt' ) , 'utf8' ) ;
212+
213+ // Extract words from the README.txt "YOUR 25 RECOVERY WORDS:" section
214+ const wordsMatch = bobReadme . match ( / Y O U R 2 5 R E C O V E R Y W O R D S : \n \n ( [ \s \S ] * ?) \n \n R e a d t h e s e w o r d s / ) ;
215+ expect ( wordsMatch ) . not . toBeNull ( ) ;
216+
217+ // Parse the two-column word grid into ordered word list
218+ const wordLines = wordsMatch ! [ 1 ] . trim ( ) . split ( '\n' ) ;
219+ const leftWords : string [ ] = [ ] ;
220+ const rightWords : string [ ] = [ ] ;
221+ const half = 13 ; // 25 words: 13 left (1-13), 12 right (14-25)
222+ for ( const line of wordLines ) {
223+ // Each line has format: " 1. word 14. word"
224+ const matches = line . match ( / \d + \. \s + ( \S + ) / g) ;
225+ if ( matches ) {
226+ for ( const m of matches ) {
227+ const wordMatch = m . match ( / ( \d + ) \. \s + ( \S + ) / ) ;
228+ if ( wordMatch ) {
229+ const idx = parseInt ( wordMatch [ 1 ] , 10 ) ;
230+ const word = wordMatch [ 2 ] ;
231+ if ( idx <= half ) {
232+ leftWords . push ( word ) ;
233+ } else {
234+ rightWords . push ( word ) ;
235+ }
236+ }
237+ }
238+ }
239+ }
240+ // Combine: left column (1-13) then right column (14-25)
241+ const words = [ ...leftWords , ...rightWords ] . join ( ' ' ) ;
242+ expect ( words . split ( ' ' ) . length ) . toBe ( 25 ) ;
243+
244+ // Type the 25 words into the paste area (includes index as 25th word)
245+ await recovery . clickPasteButton ( ) ;
246+ await recovery . expectPasteAreaVisible ( ) ;
247+ await recovery . pasteShare ( words ) ;
248+ await recovery . submitPaste ( ) ;
249+
250+ // Bob's share should now be added (index extracted from 25th word)
251+ await recovery . expectShareCount ( 2 ) ;
252+ } ) ;
253+
254+ test ( 'paste area accepts numbered word grid directly' , async ( { page } ) => {
255+ const [ aliceDir , bobDir ] = extractBundles ( bundlesDir , [ 'Alice' , 'Bob' ] ) ;
256+ const recovery = new RecoveryPage ( page , aliceDir ) ;
257+
258+ await recovery . open ( ) ;
259+ await recovery . expectShareCount ( 1 ) ;
260+
261+ // Read Bob's README.txt and extract the word grid section as-is
262+ const bobReadme = fs . readFileSync ( path . join ( bobDir , 'README.txt' ) , 'utf8' ) ;
263+ const wordsMatch = bobReadme . match ( / Y O U R 2 5 R E C O V E R Y W O R D S : \n \n ( [ \s \S ] * ?) \n \n R e a d t h e s e w o r d s / ) ;
264+ expect ( wordsMatch ) . not . toBeNull ( ) ;
265+ const wordGrid = wordsMatch ! [ 1 ] ; // The numbered two-column grid
266+
267+ // Paste the word grid into the paste area
268+ await recovery . clickPasteButton ( ) ;
269+ await recovery . expectPasteAreaVisible ( ) ;
270+ await recovery . pasteShare ( wordGrid ) ;
271+ await recovery . submitPaste ( ) ;
272+
273+ // Share should be added directly (index from 25th word, no manual input needed)
274+ await recovery . expectShareCount ( 2 ) ;
275+ } ) ;
276+
201277 test ( 'detects duplicate shares' , async ( { page } ) => {
202278 const bundleDir = extractBundle ( bundlesDir , 'Alice' ) ;
203279 const recovery = new RecoveryPage ( page , bundleDir ) ;
0 commit comments