You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							324 lines
						
					
					
						
							15 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							324 lines
						
					
					
						
							15 KiB
						
					
					
				| /** | |
|  * End-to-End Contact Management Tests | |
|  *  | |
|  * Comprehensive test suite for Time Safari's contact management and gift recording features. | |
|  * Tests run sequentially to avoid state conflicts and API rate limits. | |
|  *  | |
|  * Test Flow: | |
|  * 1. Contact Creation & Verification | |
|  *    - Add contact using DID | |
|  *    - Verify contact appears in list | |
|  *    - Rename contact and verify change | |
|  *    - Check contact appears in "Record Something" section | |
|  *  | |
|  * 2. Gift Recording Flow | |
|  *    - Generate unique gift details | |
|  *    - Record gift to contact | |
|  *    - Verify gift confirmation | |
|  *    - Check gift appears in activity feed | |
|  *  | |
|  * 3. Contact Import/Export Tests | |
|  *    - Copy contact details to clipboard | |
|  *    - Delete existing contact | |
|  *    - Import contact from clipboard | |
|  *    - Verify imported contact details | |
|  *  | |
|  * Test Data Generation: | |
|  * - Gift titles: "Gift " + 16-char random string | |
|  * - Gift amounts: Random 1-99 value | |
|  * - Contact names: Predefined test values | |
|  * - DIDs: Uses test accounts (e.g., did:ethr:0x000...) | |
|  *  | |
|  * Key Selectors: | |
|  * - Contact list: 'li[data-testid="contactListItem"]' | |
|  * - Gift recording: '#sectionRecordSomethingGiven' | |
|  * - Contact name: '[data-testid="contactName"] input' | |
|  * - Alert dialogs: 'div[role="alert"]' | |
|  *  | |
|  * Timeouts & Retries: | |
|  * - Uses OS-specific timeouts (longer for Linux) | |
|  * - Implements retry logic for network operations | |
|  * - Waits for UI animations and state changes | |
|  *  | |
|  * Alert Handling: | |
|  * - Closes onboarding dialogs | |
|  * - Handles registration prompts | |
|  * - Verifies alert dismissal | |
|  *  | |
|  * State Requirements: | |
|  * - Clean database state | |
|  * - No existing contacts for test DIDs | |
|  * - Available API rate limits | |
|  *  | |
|  * @example Basic contact addition | |
|  * ```typescript | |
|  * await page.goto('./contacts'); | |
|  * await page.getByPlaceholder('URL or DID, Name, Public Key') | |
|  *   .fill('did:ethr:0x000...., User Name'); | |
|  * await page.locator('button > svg.fa-plus').click(); | |
|  * ``` | |
|  */ | |
| 
 | |
| import { test, expect, Page } from '@playwright/test'; | |
| import { importUser, getOSSpecificTimeout } from './testUtils'; | |
| 
 | |
| test('Add contact, record gift, confirm gift', async ({ page }) => { | |
| 
 | |
|   // Generate a random string of 16 characters | |
|   let randomString = Math.random().toString(36).substring(2, 18); | |
| 
 | |
|   // In case the string is shorter than 16 characters, generate more characters until it is 16 characters long | |
|   while (randomString.length < 16) { | |
|     randomString += Math.random().toString(36).substring(2, 18); | |
|   } | |
|   const finalRandomString = randomString.substring(0, 16); | |
| 
 | |
|   // Generate a random non-zero single-digit number | |
|   const randomNonZeroNumber = Math.floor(Math.random() * 99) + 1; | |
| 
 | |
|   // Standard title prefix | |
|   const standardTitle = 'Gift '; | |
| 
 | |
|   // Combine title prefix with the random string | |
|   const finalTitle = standardTitle + finalRandomString; | |
| 
 | |
|   const contactName = 'Contact #000 renamed'; | |
|   const userName = 'User #000'; | |
| 
 | |
|   // Import user 01 | |
|   await importUser(page, '01'); | |
| 
 | |
|   // Add new contact | |
|   await page.goto('./contacts'); | |
|   await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F, ' + userName); | |
|   await page.locator('button > svg.fa-plus').click(); | |
|   // Commenting the following lines because user 00 is already registered | |
|   // await expect(page.locator('div[role="alert"] span:has-text("No")')).toBeVisible(); | |
|   // await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register | |
|   await expect(page.locator('div[role="alert"] span:has-text("Success")')).toBeVisible(); | |
|   await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert | |
|   await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone | |
|  | |
|   // Verify added contact | |
|   await expect(page.locator('li.border-b')).toContainText(userName); | |
| 
 | |
|   // Rename contact | |
|   await page.locator(`li[data-testid="contactListItem"] h2:has-text("${userName}") + div svg.fa-circle-info`).click(); | |
|   // now on the DID view page | |
|   await page.locator('h2 svg.fa-pen').click(); | |
|   // now on the contact edit page | |
|   await expect(page.getByTestId('contactName').locator('input')).toBeVisible(); | |
|   // check that the input field has userName | |
|   await expect(page.getByTestId('contactName').locator('input')).toHaveValue(userName); | |
|   await page.getByTestId('contactName').locator('input').fill(contactName); | |
|   await page.getByRole('button', { name: 'Save' }).click(); | |
|   await expect(page.locator('h2', { hasText: contactName })).toBeVisible(); | |
| 
 | |
|   // Confirm that home shows contact in "Record Something…" | |
|   await page.goto('./'); | |
|   await page.getByTestId('closeOnboardingAndFinish').click(); | |
|   await page.getByRole('button', { name: 'Person' }).click(); | |
|   await expect(page.locator('#sectionGiftedGiver').getByRole('listitem').filter({ hasText: contactName })).toBeVisible(); | |
| 
 | |
|   // Record something given by new contact | |
|   await page.locator('#sectionGiftedGiver').getByRole('listitem').filter({ hasText: contactName }).click(); | |
|   await page.getByPlaceholder('What was given').fill(finalTitle); | |
|   await page.getByRole('spinbutton').fill(randomNonZeroNumber.toString()); | |
|   await page.getByRole('button', { name: 'Sign & Send' }).click(); | |
|   await expect(page.getByText('That gift was recorded.')).toBeVisible(); | |
| 
 | |
|   // Refresh home view and check gift | |
|   await page.goto('./'); | |
| 
 | |
|   // Firefox complains on load the initial feed here when we use the test server. | |
|   // It may be similar to the CORS problem below. | |
|   const item = await page.locator('li:first-child').filter({ hasText: finalTitle }); | |
|   await item.locator('[data-testid="circle-info-link"]').click(); | |
|   await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible(); | |
|   await expect(page.getByText(finalTitle, { exact: true })).toBeVisible(); | |
| 
 | |
|   // Switch to user 00 | |
|   await page.goto('./account'); | |
|   await page.getByRole('heading', { name: 'Advanced' }).click(); | |
| 
 | |
|   const switchIdentifierLink = page.getByRole('link', { name: 'Switch Identifier' }); | |
|   await expect(switchIdentifierLink).toBeVisible(); | |
|   await switchIdentifierLink.click(); | |
| 
 | |
|   const addAnotherIdentityLink = page.getByRole('link', { name: 'Add Another Identity…' }); | |
|   await expect(addAnotherIdentityLink).toBeVisible(); | |
|   await addAnotherIdentityLink.click(); | |
| 
 | |
|   await page.getByText('You have a seed').click(); | |
|   await page.getByPlaceholder('Seed Phrase').fill('rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage'); | |
|   await page.getByRole('button', { name: 'Import' }).click(); | |
|   await expect(page.locator('[data-testid="didWrapper"]').getByRole('code')).toContainText('did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F'); | |
| 
 | |
|   // Go to home view and look for gift | |
|   await page.goto('./'); | |
|   // await page.getByTestId('closeOnboardingAndFinish').click(); | |
|   const giftLink = page.locator('li:first-child').filter({ hasText: finalTitle }).locator('[data-testid="circle-info-link"]'); | |
|   await expect(giftLink).toBeVisible(); | |
|   await giftLink.click(); | |
| 
 | |
|   // Confirm gift as user 00 | |
|   await page.getByTestId('confirmGiftLink').click(); | |
|   await page.getByRole('button', { name: 'Confirm' }).click(); | |
|   await page.getByRole('button', { name: 'Yes' }).click(); | |
|   await expect(page.getByText('Confirmation submitted.')).toBeVisible(); | |
|   await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert | |
|  | |
|   // Refresh claim page, Confirm button should throw an alert because they already confirmed | |
|   await page.reload(); | |
|   await page.getByRole('button', { name: 'Confirm' }).click(); | |
|   await expect(page.locator('div[role="alert"]')).toBeVisible(); | |
| }); | |
| 
 | |
| test('Without being registered, add contacts without registration', async ({ page, context }) => { | |
|   // Add new contact without registering | |
|   await page.goto('./contacts'); | |
|   await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x111d15564f824D56C7a07b913aA7aDd03382aA39, User #111'); | |
|   await page.locator('button > svg.fa-plus').click(); | |
|   await expect(page.locator('div[role="alert"] span:has-text("Success")')).toBeVisible(); | |
|   await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert | |
|   // wait for the alert to disappear, which also ensures that there is no "Register" button waiting | |
|   await expect(page.locator('div[role="alert"]')).toBeHidden(); | |
| 
 | |
| }); | |
| 
 | |
| test('Add contact, copy details, delete, and import from paste & from file', async ({ page, context }) => { | |
|   await importUser(page, '00'); | |
| 
 | |
|   // Add new contact | |
|   await page.goto('./contacts'); | |
|   await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x111d15564f824D56C7a07b913aA7aDd03382aA39, User #111'); | |
|   await page.locator('button > svg.fa-plus').click(); | |
|   await expect(page.locator('div[role="alert"]')).toBeVisible(); | |
|   await expect(page.locator('div[role="alert"] span:has-text("No")')).toBeVisible(); | |
|   await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register | |
|   await expect(page.locator('div[role="alert"] span:has-text("Success")')).toBeVisible(); | |
|   await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert | |
|   // wait for the alert to disappear | |
|   await expect(page.locator('div[role="alert"]')).toBeHidden(); | |
| 
 | |
|   // Add another new contact | |
|   await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x222BB77E6Ff3774d34c751f3c1260866357B677b, User #222, asdf1234'); | |
|   await page.locator('button > svg.fa-plus').click(); | |
|   await expect(page.locator('div[role="alert"] span:has-text("No")')).toBeVisible(); | |
|   await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register | |
|   await expect(page.locator('div[role="alert"] span:has-text("Success")')).toBeVisible(); | |
|   await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert | |
|   await expect(page.locator('div[role="alert"]')).toBeHidden(); | |
| 
 | |
|   await expect(page.getByTestId('contactListItem')).toHaveCount(2); | |
| 
 | |
|   //// Copy contact details, export them, remove them, and paste to add them | |
|  | |
|   // Copy contact details | |
|   await page.getByTestId('contactCheckAllTop').click(); | |
|   await page.getByTestId('copySelectedContactsButtonTop').click(); | |
|   await expect(page.locator('div[role="alert"]')).toBeHidden(); | |
|   // I would prefer to copy from the clipboard, but the recommended approaches don't work. | |
|   // See a different clipboard solution below. | |
|  | |
|   // see contact details on the second contact | |
|   await page.getByTestId('contactListItem').nth(1).locator('h2 > a').click(); | |
|   await page.getByRole('heading', { name: 'Identifier Details' }).isVisible(); | |
|   // remove contact | |
|   await page.locator('button > svg.fa-trash-can').click(); | |
|   await page.locator('div[role="alert"] button:has-text("Yes")').click(); | |
|   // for some reason, .isHidden() (without expect) doesn't work | |
|   await expect(page.locator('div[role="alert"] button:has-text("Yes")')).toBeHidden(); | |
| 
 | |
|   // Firefox has a problem when we run this against the test server. It doesn't load the feed. | |
|   // It says there's a CORS problem; maybe it's more strict than the other browsers. | |
|   // It works when we set the config to use a local server. | |
|   // Seems like we hit a similar problem above. | |
|   await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss alert | |
|   await expect(page.getByRole('alert').filter({ hasText: 'Success Contact has been' })).toBeHidden(); | |
| 
 | |
|   // go to the contacts page and paste the copied contact details | |
|   await page.goto('./contacts'); | |
|   // check that there are fewer contacts | |
|   await expect(page.getByTestId('contactListItem')).toHaveCount(1); | |
| 
 | |
|   const contactData = 'Paste this: [{ "did": "did:ethr:0x111d15564f824D56C7a07b913aA7aDd03382aA39", "name": "User #111" }, { "did": "did:ethr:0x222BB77E6Ff3774d34c751f3c1260866357B677b", "name": "User #222", "publicKeyBase64": "asdf1234"}] ' | |
|   await page.getByPlaceholder('URL or DID, Name, Public Key').fill(contactData); | |
|   await page.locator('button > svg.fa-plus').click(); | |
|   // we're on the contact-import page | |
|   await expect(page.locator('li', { hasText: 'New' })).toHaveCount(1); | |
|   await expect(page.locator('span').filter({ hasText: 'the same as' })).toBeVisible(); | |
|   await page.locator('button', { hasText: 'Import' }).click(); | |
|   // check that there are more contacts | |
|   await expect(page.getByTestId('contactListItem')).toHaveCount(2); | |
| 
 | |
|   // Import via the file backup-import, with both new and existing contacts | |
|   await page.goto('./account'); | |
|   await page.getByRole('heading', { name: 'Advanced' }).click(); | |
|   const fileSelect = await page.locator('input[type="file"]') | |
|   fileSelect.setInputFiles('./test-playwright/exported-data.json'); | |
|   await page.locator('button', { hasText: 'Import Contacts' }).click(); | |
|   // we're on the contact-import page | |
|   await expect(page.locator('li', { hasText: '- New' })).toHaveCount(3); | |
|   await expect(page.locator('li', { hasText: '- Existing' })).toHaveCount(1); | |
|   await expect(page.locator('span').filter({ hasText: 'the same as' })).toBeHidden(); | |
|   await page.locator('button', { hasText: 'Import' }).click(); | |
|   // check that there are more contacts | |
|   await expect(page.getByTestId('contactListItem')).toHaveCount(5); | |
|   // The visibility error is because currently the server returns an error for the same person. | |
|   // But it should only show that one, for User #000. | |
|  | |
| }); | |
| 
 | |
| test('Copy contact to clipboard, then import ', async ({ page, context }, testInfo) => { | |
|   await importUser(page, '00'); | |
| 
 | |
|   await page.goto('./account'); | |
|   await page.getByRole('heading', { name: 'Advanced' }).click(); | |
|   const fileSelect = await page.locator('input[type="file"]') | |
|   fileSelect.setInputFiles('./test-playwright/exported-data.json'); | |
|   await page.locator('button', { hasText: 'Import Contacts' }).click(); | |
|   // we're on the contact-import page | |
|   await expect(page.getByRole('heading', { name: "Contact Import" })).toBeVisible(); | |
|   await page.locator('button', { hasText: 'Import' }).click(); | |
| 
 | |
|   await page.goto('./contacts'); | |
|   // Copy contact details | |
|   await page.getByTestId('contactCheckAllTop').click(); | |
| 
 | |
|   const isFirefox = await page.evaluate(() => { | |
|     return navigator.userAgent.includes('Firefox'); | |
|   }); | |
|   if (isFirefox) { | |
|     // Firefox doesn't grant permissions like this but it works anyway. | |
|   } else { | |
|     await context.grantPermissions(['clipboard-read']); | |
|   } | |
| 
 | |
|   const isWebkit = await page.evaluate(() => { | |
|     return navigator.userAgent.includes('Macintosh') || navigator.userAgent.includes('iPhone'); | |
|   }); | |
|   if (isWebkit) { | |
|     console.log("Haven't found a way to access clipboard text in Webkit. Skipping."); | |
|     return; | |
|   } | |
| 
 | |
|   await page.getByTestId('copySelectedContactsButtonTop').click(); | |
|   const clipboardText = await page.evaluate(async () => { | |
|     return navigator.clipboard.readText(); | |
|   }); | |
| 
 | |
|   // look into the playwright.config file for the server URL | |
|   const webServer = testInfo.config.webServer; | |
|   const clientServerUrl = webServer?.url; | |
| 
 | |
|   const PATH_PART = clientServerUrl + "/deep-link/contact-import/"; | |
|   await expect(clipboardText).toContain(PATH_PART); | |
| 
 | |
|   await expect(page.locator('div[role="alert"]')).toBeHidden({ timeout: 7000 }); | |
| 
 | |
|   await page.goto(clipboardText); | |
|   // we're on the contact-import page | |
|   await expect(page.getByRole('heading', { name: "Contact Import" })).toBeVisible(); | |
|   await expect(page.locator('span', { hasText: '4 contacts are the same' })).toBeVisible(); | |
| });
 | |
| 
 |