forked from jsnbuchanan/crowd-funder-for-time-pwa
- Add detailed header documentation to playwright tests - Document test categories and flows - Add key selector documentation - Document state verification and alert handling - Include code examples and usage patterns - Add important checks and requirements The documentation helps developers understand the foundational tests that verify basic application functionality before running more complex test suites.
324 lines
15 KiB
TypeScript
324 lines
15 KiB
TypeScript
/**
|
|
* 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();
|
|
await expect(page.locator('div[role="alert"] span:has-text("Contact Added")')).toBeVisible();
|
|
await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register
|
|
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}") + span 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 expect(page.locator('#sectionRecordSomethingGiven ul li').filter({ hasText: contactName }).nth(0)).toBeVisible();
|
|
|
|
// Record something given by new contact
|
|
await page.getByRole('heading', { name: 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.
|
|
await page.locator('li').filter({ hasText: finalTitle }).locator('a').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();
|
|
await page.getByRole('link', { name: 'Switch Identifier' }).click();
|
|
await page.getByRole('link', { name: 'Add Another Identity…' }).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.getByRole('code')).toContainText('did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F');
|
|
|
|
// Go to home view and look for gift
|
|
await page.goto('./');
|
|
await page.getByTestId('closeOnboardingAndFinish').click();
|
|
await page.locator('li').filter({ hasText: finalTitle }).locator('a').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 }) => {
|
|
await page.goto('./account');
|
|
// wait until the DID shows on the page in the 'did' element
|
|
const didElem = await page.getByTestId('didWrapper').locator('code');
|
|
const newDid = await didElem.innerText();
|
|
expect(newDid.trim()).toEqual('');
|
|
|
|
// 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("Contact Added")')).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 page.locator('div[role="alert"] button:has-text("No")').click(); // don't register
|
|
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"]')).toBeVisible();
|
|
await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register
|
|
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 page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss alert
|
|
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('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.locator('div[role="alert"]')).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 Only 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 Only 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();
|
|
|
|
// // There's a crazy amount of overlap in all the userAgent values. Ug.
|
|
// const agent = await page.evaluate(() => {
|
|
// return navigator.userAgent;
|
|
// });
|
|
// console.log("agent: ", agent);
|
|
|
|
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;
|
|
}
|
|
|
|
console.log("Running test that copies contact details to clipboard.");
|
|
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 + "/contact-import/";
|
|
expect(clipboardText).toContain(PATH_PART);
|
|
|
|
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss alert
|
|
await expect(page.locator('div[role="alert"]')).toBeHidden();
|
|
|
|
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();
|
|
});
|