forked from trent_larson/crowd-funder-for-time-pwa
Fix duplicate export declarations and migrate ContactsView with sub-components
- Remove duplicate NOTIFY_INVITE_MISSING and NOTIFY_INVITE_PROCESSING_ERROR exports - Update InviteOneAcceptView.vue to use correct NOTIFY_INVITE_TRUNCATED_DATA constant - Migrate ContactsView to PlatformServiceMixin and extract into modular sub-components - Resolves TypeScript compilation errors preventing web build
This commit is contained in:
@@ -69,7 +69,7 @@
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { deleteContact, generateAndRegisterEthrUser, importUser, importUserAndCloseOnboarding } from './testUtils';
|
||||
import { deleteContact, generateAndRegisterEthrUser, importUser } from './testUtils';
|
||||
|
||||
test('Check activity feed - check that server is running', async ({ page }) => {
|
||||
// Load app homepage
|
||||
@@ -144,97 +144,32 @@ test('Check ID generation', async ({ page }) => {
|
||||
|
||||
|
||||
test('Check setting name & sharing info', async ({ page }) => {
|
||||
// Do NOT import a user; start with a fresh, unregistered user state
|
||||
|
||||
function now() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
// Start by loading the homepage and looking for the onboarding notice and button
|
||||
// Load homepage to trigger ID generation (?)
|
||||
await page.goto('./');
|
||||
|
||||
// Wait for page to fully load and check for overlays
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Loop to close all visible overlays/dialogs before proceeding
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const overlayCount = await page.locator('.dialog-overlay').count();
|
||||
if (overlayCount === 0) break;
|
||||
|
||||
// Try to close the overlay with various known close button texts
|
||||
const closeButtons = [
|
||||
"That's enough help, thanks.",
|
||||
'Close',
|
||||
'Cancel',
|
||||
'Dismiss',
|
||||
'Got it',
|
||||
'OK'
|
||||
];
|
||||
|
||||
let closed = false;
|
||||
for (const buttonText of closeButtons) {
|
||||
const button = page.getByRole('button', { name: buttonText });
|
||||
if (await button.count() > 0) {
|
||||
await button.click();
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no text button found, try the close icon (xmark)
|
||||
if (!closed) {
|
||||
const closeIcon = page.locator('.fa-xmark, .fa-times, [aria-label*="close"], [aria-label*="Close"]');
|
||||
if (await closeIcon.count() > 0) {
|
||||
await closeIcon.first().click();
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!closed) break;
|
||||
|
||||
// Wait a bit for the overlay to close
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
await page.getByTestId('closeOnboardingAndFinish').click();
|
||||
// Check 'someone must register you' notice
|
||||
await expect(page.getByText('someone must register you.')).toBeVisible();
|
||||
|
||||
// Click the "Show them" button
|
||||
await page.getByRole('button', { name: 'Show them' }).click();
|
||||
|
||||
// Wait for the "Set Your Name" dialog to appear
|
||||
await page.getByRole('button', { name: /Show them/}).click();
|
||||
// fill in a name
|
||||
await expect(page.getByText('Set Your Name')).toBeVisible();
|
||||
|
||||
// Fill in the name
|
||||
await page.getByRole('textbox').fill('Test User');
|
||||
|
||||
// Click Save
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
// Wait for the choice dialog to appear
|
||||
await expect(page.getByText('We will share some other way')).toBeVisible();
|
||||
|
||||
// Click "We will share some other way"
|
||||
await page.getByRole('button', { name: 'We will share some other way' }).click();
|
||||
|
||||
// Wait up to 10 seconds for the heading
|
||||
await expect(page.getByRole('heading', { name: 'Share Your Contact Info' })).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Click the Copy to Clipboard button
|
||||
await expect(page.getByRole('button', { name: 'Copy contact information to clipboard' })).toBeVisible({ timeout: 10000 });
|
||||
await page.getByRole('button', { name: 'Copy contact information to clipboard' }).click();
|
||||
|
||||
// Wait for either the notification or navigation to contacts
|
||||
try {
|
||||
await expect(page.getByText('contact info was copied')).toBeVisible({ timeout: 10000 });
|
||||
} catch {
|
||||
await expect(page.getByText('your contacts')).toBeVisible({ timeout: 10000 });
|
||||
}
|
||||
await page.getByRole('textbox').fill('Me Test User');
|
||||
await page.locator('button:has-text("Save")').click();
|
||||
await expect(page.getByText('share some other way')).toBeVisible();
|
||||
await page.getByRole('button', { name: /share some other way/ }).click();
|
||||
await expect(page.getByRole('button', { name: 'copy to clipboard' })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'copy to clipboard' }).click();
|
||||
await expect(page.getByText('contact info was copied')).toBeVisible();
|
||||
// dismiss alert and wait for it to go away
|
||||
await page.locator('div[role="alert"] button > svg.fa-xmark').click();
|
||||
await expect(page.getByText('contact info was copied')).toBeHidden();
|
||||
// check that they're on the Contacts screen
|
||||
await expect(page.getByText('your contacts')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Confirm test API setting (may fail if you are running your own Time Safari)', async ({ page }, testInfo) => {
|
||||
// Load account view
|
||||
await page.goto('./account');
|
||||
await page.getByRole('heading', { name: 'Advanced' }).click();
|
||||
await page.getByTestId('advancedSettings').click();
|
||||
|
||||
// look into the config file: if it starts Time Safari, it might say which server it should set by default
|
||||
const webServer = testInfo.config.webServer;
|
||||
@@ -243,24 +178,8 @@ test('Confirm test API setting (may fail if you are running your own Time Safari
|
||||
const endorserTerm = endorserWords?.find(word => word.startsWith(ENDORSER_ENV_NAME + '='));
|
||||
const endorserTermInConfig = endorserTerm?.substring(ENDORSER_ENV_NAME.length + 1);
|
||||
|
||||
const expectedEndorserServer = endorserTermInConfig || 'https://test-api.endorser.ch';
|
||||
|
||||
// Get the actual value from the input field
|
||||
const actualValue = await page.locator('#apiServerInput').inputValue();
|
||||
|
||||
// Check if the field has a value (not empty)
|
||||
if (actualValue) {
|
||||
// If it has a value, check if it matches the expected server (allowing for localhost/127.0.0.1 variations)
|
||||
const normalizedExpected = expectedEndorserServer.replace('localhost', '127.0.0.1');
|
||||
const normalizedActual = actualValue.replace('localhost', '127.0.0.1');
|
||||
|
||||
if (normalizedExpected !== normalizedActual) {
|
||||
throw new Error(`API server mismatch. Expected: "${expectedEndorserServer}" (or localhost equivalent), Got: "${actualValue}"`);
|
||||
}
|
||||
} else {
|
||||
// If the field is empty, that's also acceptable (might be using default)
|
||||
// Field is empty, which is acceptable for default configuration
|
||||
}
|
||||
const endorserServer = endorserTermInConfig || 'https://test-api.endorser.ch';
|
||||
await expect(page.locator('#apiServerInput')).toHaveValue(endorserServer);
|
||||
});
|
||||
|
||||
test('Check User 0 can register a random person', async ({ page }) => {
|
||||
@@ -270,13 +189,7 @@ test('Check User 0 can register a random person', async ({ page }) => {
|
||||
|
||||
await page.goto('./');
|
||||
await page.getByTestId('closeOnboardingAndFinish').click();
|
||||
|
||||
// Click the "Person" button to open the gift recording dialog
|
||||
await page.getByRole('button', { name: 'Person' }).click();
|
||||
|
||||
// In the dialog, click on "Unnamed" to select it as the giver
|
||||
await page.getByRole('heading', { name: 'Unnamed' }).first().click();
|
||||
|
||||
await page.getByRole('heading', { name: 'Unnamed/Unknown' }).click();
|
||||
await page.getByPlaceholder('What was given').fill('Gave me access!');
|
||||
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||
await expect(page.getByText('That gift was recorded.')).toBeVisible();
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
* @requires ./testUtils - For user management utilities
|
||||
*/
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { deleteContact, generateAndRegisterEthrUser, generateRandomString, importUser, switchToUser } from './testUtils';
|
||||
import { deleteContact, generateNewEthrUser, generateRandomString, importUser, switchToUser } from './testUtils';
|
||||
|
||||
test('Check User 0 can invite someone', async ({ page }) => {
|
||||
await importUser(page, '00');
|
||||
@@ -51,37 +51,9 @@ test('Check User 0 can invite someone', async ({ page }) => {
|
||||
expect(inviteLink).not.toBeNull();
|
||||
|
||||
// become the new user and accept the invite
|
||||
const newDid = await generateAndRegisterEthrUser(page);
|
||||
const newDid = await generateNewEthrUser(page);
|
||||
await switchToUser(page, newDid);
|
||||
|
||||
// Extract the JWT from the invite link and navigate to local development server
|
||||
const jwt = inviteLink?.split('/').pop();
|
||||
if (!jwt) {
|
||||
throw new Error('Could not extract JWT from invite link');
|
||||
}
|
||||
await page.goto(`./deep-link/invite-one-accept/${jwt}`);
|
||||
|
||||
// Wait for redirect to contacts page and dialog to appear
|
||||
await page.waitForURL('**/contacts**');
|
||||
|
||||
// Wait a bit for any processing to complete
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Check if the dialog appears, if not, check for registration errors
|
||||
const dialogVisible = await page.locator('input[placeholder="Name"]').isVisible().catch(() => false);
|
||||
|
||||
if (!dialogVisible) {
|
||||
const bodyText = await page.locator('body').textContent();
|
||||
|
||||
// Check if this is a registration error
|
||||
if (bodyText?.includes('not registered to make claims')) {
|
||||
test.skip(true, 'User #0 not registered on test server. Please ensure endorser server is running and User #0 is registered.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the dialog to appear and then fill the name
|
||||
await page.waitForSelector('input[placeholder="Name"]', { timeout: 10000 });
|
||||
await page.goto(inviteLink as string);
|
||||
await page.getByPlaceholder('Name', { exact: true }).fill(`My pal User #0`);
|
||||
await page.locator('button:has-text("Save")').click();
|
||||
await expect(page.locator('button:has-text("Save")')).toBeHidden();
|
||||
|
||||
@@ -69,37 +69,14 @@ test('Check usage limits', async ({ page }) => {
|
||||
// Import user 01
|
||||
const did = await importUser(page, '01');
|
||||
|
||||
// Wait for the page to load
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Verify that "Usage Limits" section is visible
|
||||
await expect(page.locator('#sectionUsageLimits')).toBeVisible();
|
||||
|
||||
// Click "Recheck Limits" to trigger limits loading
|
||||
await page.getByRole('button', { name: 'Recheck Limits' }).click();
|
||||
|
||||
// Wait for limits to load (either success or error message)
|
||||
await page.waitForTimeout(3000);
|
||||
const updatedUsageLimitsText = await page.locator('#sectionUsageLimits').textContent();
|
||||
|
||||
// Check if limits loaded successfully or show error message
|
||||
const hasLimitsData = updatedUsageLimitsText?.includes('You have done') ||
|
||||
updatedUsageLimitsText?.includes('You have uploaded') ||
|
||||
updatedUsageLimitsText?.includes('No limits were found') ||
|
||||
updatedUsageLimitsText?.includes('You have no identifier');
|
||||
|
||||
if (hasLimitsData) {
|
||||
// Limits loaded successfully, continue with original test
|
||||
await expect(page.locator('#sectionUsageLimits')).toContainText('You have done');
|
||||
await expect(page.locator('#sectionUsageLimits')).toContainText('You have uploaded');
|
||||
|
||||
// These texts only appear when limits are successfully loaded
|
||||
await expect(page.getByText('Your claims counter resets')).toBeVisible();
|
||||
await expect(page.getByText('Your registration counter resets')).toBeVisible();
|
||||
await expect(page.getByText('Your image counter resets')).toBeVisible();
|
||||
}
|
||||
|
||||
// The Recheck Limits button should always be visible
|
||||
await expect(page.locator('#sectionUsageLimits')).toContainText('You have done');
|
||||
await expect(page.locator('#sectionUsageLimits')).toContainText('You have uploaded');
|
||||
|
||||
await expect(page.getByText('Your claims counter resets')).toBeVisible();
|
||||
await expect(page.getByText('Your registration counter resets')).toBeVisible();
|
||||
await expect(page.getByText('Your image counter resets')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Recheck Limits' })).toBeVisible();
|
||||
|
||||
// Set name
|
||||
|
||||
@@ -100,15 +100,7 @@ test('Record something given', async ({ page }) => {
|
||||
// Record something given
|
||||
await page.goto('./');
|
||||
await page.getByTestId('closeOnboardingAndFinish').click();
|
||||
|
||||
// Click the "Person" button to open the gift recording dialog
|
||||
await page.getByRole('button', { name: 'Person' }).click();
|
||||
|
||||
// In the dialog, click on "Unnamed" to select it as the giver
|
||||
// Use the first "Unnamed" element which should be in the entity grid
|
||||
await page.getByRole('heading', { name: 'Unnamed' }).first().click();
|
||||
|
||||
// Fill in the gift details
|
||||
await page.getByRole('heading', { name: 'Unnamed/Unknown' }).click();
|
||||
await page.getByPlaceholder('What was given').fill(finalTitle);
|
||||
await page.getByRole('spinbutton').fill(randomNonZeroNumber.toString());
|
||||
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||
@@ -118,7 +110,7 @@ test('Record something given', async ({ page }) => {
|
||||
// Refresh home view and check gift
|
||||
await page.goto('./');
|
||||
const item = await page.locator('li').filter({ hasText: finalTitle });
|
||||
await item.locator('[data-testid="circle-info-link"]').first().click();
|
||||
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();
|
||||
const page1Promise = page.waitForEvent('popup');
|
||||
|
||||
@@ -115,13 +115,7 @@ test('Record 9 new gifts', async ({ page }) => {
|
||||
if (i === 0) {
|
||||
await page.getByTestId('closeOnboardingAndFinish').click();
|
||||
}
|
||||
|
||||
// Click the "Person" button to open the gift recording dialog
|
||||
await page.getByRole('button', { name: 'Person' }).click();
|
||||
|
||||
// In the dialog, click on "Unnamed" to select it as the giver
|
||||
await page.getByRole('heading', { name: 'Unnamed' }).first().click();
|
||||
|
||||
await page.getByRole('heading', { name: 'Unnamed/Unknown' }).click();
|
||||
await page.getByPlaceholder('What was given').fill(finalTitles[i]);
|
||||
await page.getByRole('spinbutton').fill(finalNumbers[i].toString());
|
||||
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||
|
||||
@@ -93,3 +93,35 @@ test('Record item given from image-share', async ({ page }) => {
|
||||
const item1 = page.locator('li').filter({ hasText: finalTitle });
|
||||
await expect(item1.getByRole('img')).toBeVisible();
|
||||
});
|
||||
|
||||
// // I believe there's a way to test this service worker feature.
|
||||
// // The following is what I got from ChatGPT. I wonder if it doesn't work because it's not registering the service worker correctly.
|
||||
//
|
||||
// test('Trigger a photo-sharing fetch event in service worker with POST to /share-target', async ({ page }) => {
|
||||
// await importUser(page, '00');
|
||||
//
|
||||
// // Create a FormData object with a photo
|
||||
// const photoPath = path.join(__dirname, '..', 'public', 'img', 'icons', 'android-chrome-192x192.png');
|
||||
// const photoContent = await fs.readFileSync(photoPath);
|
||||
// const [response] = await Promise.all([
|
||||
// page.waitForResponse(response => response.url().includes('/share-target')), // also check for response.status() === 303 ?
|
||||
// page.evaluate(async (photoContent) => {
|
||||
// const formData = new FormData();
|
||||
// formData.append('photo', new Blob([photoContent], { type: 'image/png' }), 'test-photo.jpg');
|
||||
//
|
||||
// const response = await fetch('/share-target', {
|
||||
// method: 'POST',
|
||||
// body: formData,
|
||||
// });
|
||||
//
|
||||
// return response;
|
||||
// }, photoContent)
|
||||
// ]);
|
||||
//
|
||||
// // Verify the response redirected to /shared-photo
|
||||
// //expect(response.status).toBe(303);
|
||||
// console.log('response headers', response.headers());
|
||||
// console.log('response status', response.status());
|
||||
// console.log('response url', response.url());
|
||||
// expect(response.url()).toContain('/shared-photo');
|
||||
// });
|
||||
|
||||
@@ -290,10 +290,11 @@ test('Copy contact to clipboard, then import ', async ({ page, context }, testIn
|
||||
// Copy contact details
|
||||
await page.getByTestId('contactCheckAllTop').click();
|
||||
|
||||
// Test copying contact details to clipboard
|
||||
if (process.env.BROWSER === 'webkit') {
|
||||
return;
|
||||
}
|
||||
// // 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');
|
||||
|
||||
@@ -31,43 +31,8 @@ test('Record an offer', async ({ page }) => {
|
||||
|
||||
// go to the offer and check the values
|
||||
await page.goto('./projects');
|
||||
const offersLink = page.getByRole('link', { name: 'Offers', exact: true });
|
||||
const offersLinkCount = await offersLink.count();
|
||||
|
||||
if (offersLinkCount > 0) {
|
||||
await offersLink.click();
|
||||
|
||||
// Wait for the page to load
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Check if the offers list is visible and has content
|
||||
const offersList = page.locator('#listOffers');
|
||||
const offersListVisible = await offersList.isVisible();
|
||||
|
||||
if (offersListVisible) {
|
||||
const offersListItems = await offersList.locator('li').allTextContents();
|
||||
|
||||
if (offersListItems.length === 0) {
|
||||
const emptyState = await page.getByText('You have not offered anything.').isVisible();
|
||||
|
||||
if (emptyState) {
|
||||
console.log('Offer creation succeeded but offers list is empty. This indicates a server-side issue.');
|
||||
console.log('The test environment may not be properly configured with User #0 registration.');
|
||||
console.log('Skipping this test until the server configuration is fixed.');
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('No Offers link found!');
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to find the specific offer
|
||||
const offerItem = page.locator('li').filter({ hasText: description });
|
||||
await offerItem.locator('a').first().click();
|
||||
await page.getByRole('link', { name: 'Offers', exact: true }).click();
|
||||
await page.locator('li').filter({ hasText: description }).locator('a').first().click();
|
||||
await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible();
|
||||
await expect(page.getByText(description, { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('Offered to a bigger plan')).toBeVisible();
|
||||
@@ -138,62 +103,8 @@ test('Affirm delivery of an offer', async ({ page }) => {
|
||||
// go to the home page and check that the offer is shown as new
|
||||
await importUser(page);
|
||||
await page.goto('./');
|
||||
|
||||
// Wait for page to fully load and check for overlays
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Loop to close all visible overlays/dialogs before proceeding
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const overlayCount = await page.locator('.dialog-overlay').count();
|
||||
if (overlayCount === 0) break;
|
||||
|
||||
// Try to close the overlay with various known close button texts
|
||||
const closeButtons = [
|
||||
"That's enough help, thanks.",
|
||||
'Close',
|
||||
'Cancel',
|
||||
'Dismiss',
|
||||
'Got it',
|
||||
'OK'
|
||||
];
|
||||
|
||||
let closed = false;
|
||||
for (const buttonText of closeButtons) {
|
||||
const button = page.getByRole('button', { name: buttonText });
|
||||
if (await button.count() > 0) {
|
||||
await button.click();
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no text button found, try the close icon (xmark)
|
||||
if (!closed) {
|
||||
const closeIcon = page.locator('.fa-xmark, .fa-times, [aria-label*="close"], [aria-label*="Close"]');
|
||||
if (await closeIcon.count() > 0) {
|
||||
await closeIcon.first().click();
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!closed) break;
|
||||
|
||||
// Wait a bit for the overlay to close
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
await page.getByTestId('closeOnboardingAndFinish').click();
|
||||
const offerNumElem = page.getByTestId('newOffersToUserProjectsActivityNumber');
|
||||
|
||||
// Check if there are any offers to affirm
|
||||
const offerNumText = await offerNumElem.textContent();
|
||||
if (!offerNumText || offerNumText === '0') {
|
||||
console.log('No offers available to affirm. This indicates a server-side issue.');
|
||||
console.log('The test environment may not be properly configured with User #0 registration.');
|
||||
console.log('Skipping this test until the server configuration is fixed.');
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
await expect(offerNumElem).toBeVisible();
|
||||
|
||||
// click on the number of new offers to go to the list page
|
||||
@@ -205,21 +116,8 @@ test('Affirm delivery of an offer', async ({ page }) => {
|
||||
await expect(firstItem).toBeVisible();
|
||||
await firstItem.locator('svg.fa-file-lines').click();
|
||||
await expect(page.getByText('Verifiable Claim Details', { exact: true })).toBeVisible();
|
||||
|
||||
// Check if the Affirm Delivery button is available
|
||||
const affirmButton = page.getByRole('button', { name: 'Affirm Delivery' });
|
||||
const affirmButtonCount = await affirmButton.count();
|
||||
|
||||
if (affirmButtonCount === 0) {
|
||||
console.log('Affirm Delivery button not found. This indicates a server-side issue.');
|
||||
console.log('The test environment may not be properly configured with User #0 registration.');
|
||||
console.log('Skipping this test until the server configuration is fixed.');
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// click on the 'Affirm Delivery' button
|
||||
await affirmButton.click();
|
||||
await page.getByRole('button', { name: 'Affirm Delivery' }).click();
|
||||
// fill our offer info and submit
|
||||
await page.getByPlaceholder('What was given').fill('Whatever the offer says');
|
||||
await page.getByRole('spinbutton').fill('2');
|
||||
|
||||
@@ -1,39 +1,3 @@
|
||||
/**
|
||||
* @fileoverview Tests for the new activity/offers system in TimeSafari
|
||||
*
|
||||
* CRITICAL UNDERSTANDING: Offer Acknowledgment System
|
||||
* ===================================================
|
||||
*
|
||||
* This file tests the offer acknowledgment mechanism, which was clarified through
|
||||
* systematic debugging investigation. Key findings:
|
||||
*
|
||||
* 1. POINTER-BASED TRACKING:
|
||||
* - TimeSafari uses `lastAckedOfferToUserJwtId` to track the last acknowledged offer
|
||||
* - Offers with IDs newer than this pointer are considered "new" and counted
|
||||
* - The UI shows count of offers newer than the pointer
|
||||
*
|
||||
* 2. TWO DISMISSAL MECHANISMS:
|
||||
* a) COMPLETE DISMISSAL (used in this test):
|
||||
* - Triggered by: Expanding offers section (clicking chevron)
|
||||
* - Method: expandOffersToUserAndMarkRead()
|
||||
* - Action: Sets lastAckedOfferToUserJwtId = newOffersToUser[0].jwtId (newest)
|
||||
* - Result: ALL offers marked as read, count becomes 0 (hidden)
|
||||
*
|
||||
* b) SELECTIVE DISMISSAL:
|
||||
* - Triggered by: Clicking "Keep all above as new offers"
|
||||
* - Method: markOffersAsReadStartingWith(jwtId)
|
||||
* - Action: Sets lastAckedOfferToUserJwtId = nextOffer.jwtId (partial)
|
||||
* - Result: Only offers above clicked offer marked as read
|
||||
*
|
||||
* 3. BROWSER COMPATIBILITY:
|
||||
* - Initially appeared to be Chromium-specific issue
|
||||
* - Investigation revealed test logic error, not browser incompatibility
|
||||
* - Both Chromium and Firefox now pass consistently
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @since Investigation completed 2024-12-27
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { importUser, generateNewEthrUser, switchToUser } from './testUtils';
|
||||
|
||||
@@ -48,21 +12,13 @@ test('New offers for another user', async ({ page }) => {
|
||||
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(user01Did + ', A Friend');
|
||||
await expect(page.locator('button > svg.fa-plus')).toBeVisible();
|
||||
await page.locator('button > svg.fa-plus').click();
|
||||
|
||||
// The alert shows "SuccessThey were added." not "Contact Added"
|
||||
await expect(page.locator('div[role="alert"]')).toContainText('They were added');
|
||||
|
||||
// Check if registration prompt appears (it may not if user is not registered)
|
||||
const noButtonCount = await page.locator('div[role="alert"] button:has-text("No")').count();
|
||||
if (noButtonCount > 0) {
|
||||
await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// show buttons to make offers directly to people
|
||||
await page.getByRole('button').filter({ hasText: /See Actions/i }).click();
|
||||
await page.getByRole('button').filter({ hasText: /See Hours/i }).click();
|
||||
|
||||
// make an offer directly to user 1
|
||||
// Generate a random string of 3 characters, skipping the "0." at the beginning
|
||||
@@ -88,13 +44,11 @@ test('New offers for another user', async ({ page }) => {
|
||||
// as user 1, go to the home page and check that two offers are shown as new
|
||||
await switchToUser(page, user01Did);
|
||||
await page.goto('./');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
let offerNumElemForTest = page.getByTestId('newDirectOffersActivityNumber');
|
||||
await expect(offerNumElemForTest).toHaveText('2');
|
||||
let offerNumElem = page.getByTestId('newDirectOffersActivityNumber');
|
||||
await expect(offerNumElem).toHaveText('2');
|
||||
|
||||
// click on the number of new offers to go to the list page
|
||||
await offerNumElemForTest.click();
|
||||
await offerNumElem.click();
|
||||
|
||||
await expect(page.getByText('New Offers To You', { exact: true })).toBeVisible();
|
||||
await page.getByTestId('showOffersToUser').locator('div > svg.fa-chevron-right').click();
|
||||
@@ -102,32 +56,28 @@ test('New offers for another user', async ({ page }) => {
|
||||
await expect(page.getByText(`help of ${randomString2} from #000`)).toBeVisible();
|
||||
await expect(page.getByText(`help of ${randomString1} from #000`)).toBeVisible();
|
||||
|
||||
/**
|
||||
* OFFER ACKNOWLEDGMENT MECHANISM:
|
||||
*
|
||||
* TimeSafari uses a pointer-based system to track which offers are "new":
|
||||
* - `lastAckedOfferToUserJwtId` stores the ID of the last acknowledged offer
|
||||
* - Offers newer than this pointer are considered "new" and counted
|
||||
*
|
||||
* Two dismissal mechanisms exist:
|
||||
* 1. COMPLETE DISMISSAL: Expanding the offers section calls expandOffersToUserAndMarkRead()
|
||||
* which sets lastAckedOfferToUserJwtId = newOffersToUser[0].jwtId (newest offer)
|
||||
* Result: ALL offers marked as read, count goes to 0
|
||||
*
|
||||
* 2. SELECTIVE DISMISSAL: "Keep all above" calls markOffersAsReadStartingWith(jwtId)
|
||||
* which sets lastAckedOfferToUserJwtId = nextOffer.jwtId (partial dismissal)
|
||||
* Result: Only offers above the clicked offer are marked as read
|
||||
*
|
||||
* This test uses mechanism #1 (expansion) for complete dismissal.
|
||||
* The expansion already happened when we clicked the chevron above.
|
||||
*/
|
||||
// click on the latest offer to keep it as "unread"
|
||||
await page.hover(`li:has-text("help of ${randomString2} from #000")`);
|
||||
// await page.locator('li').filter({ hasText: `help of ${randomString2} from #000` }).click();
|
||||
// await page.locator('div').filter({ hasText: /keep all above/ }).click();
|
||||
// now find the "Click to keep all above as new offers" after that list item and click it
|
||||
const liElem = page.locator('li').filter({ hasText: `help of ${randomString2} from #000` });
|
||||
await liElem.hover();
|
||||
const keepAboveAsNew = await liElem.locator('div').filter({ hasText: /keep all above/ });
|
||||
|
||||
// now see that all offers are dismissed since we expanded the section
|
||||
await keepAboveAsNew.click();
|
||||
|
||||
// now see that only one offer is shown as new
|
||||
await page.goto('./');
|
||||
await page.waitForLoadState('networkidle');
|
||||
offerNumElem = page.getByTestId('newDirectOffersActivityNumber');
|
||||
await expect(offerNumElem).toHaveText('1');
|
||||
await offerNumElem.click();
|
||||
await expect(page.getByText('New Offer To You', { exact: true })).toBeVisible();
|
||||
await page.getByTestId('showOffersToUser').locator('div > svg.fa-chevron-right').click();
|
||||
|
||||
// now see that no offers are shown as new
|
||||
await page.goto('./');
|
||||
// wait until the list with ID listLatestActivity has at least one visible item
|
||||
await page.locator('#listLatestActivity li').first().waitFor({ state: 'visible' });
|
||||
|
||||
await expect(page.getByTestId('newDirectOffersActivityNumber')).toBeHidden();
|
||||
});
|
||||
|
||||
@@ -59,106 +59,16 @@ function createContactName(did: string): string {
|
||||
return "User " + did.slice(11, 14);
|
||||
}
|
||||
|
||||
export async function deleteContact(page: Page, did: string, contactName: string) {
|
||||
// Navigate to contacts page
|
||||
export async function deleteContact(page: Page, did: string): Promise<void> {
|
||||
await page.goto('./contacts');
|
||||
|
||||
// Wait for page to load completely
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check if we need to hide the "Show Actions" view first
|
||||
const loadingCount = await page.locator('.loading-indicator').count();
|
||||
if (loadingCount > 0) {
|
||||
await page.locator('.loading-indicator').first().waitFor({ state: 'hidden' });
|
||||
}
|
||||
|
||||
// Check if "Hide Actions" button exists (meaning we're in the give numbers view)
|
||||
const showGiveNumbersExists = await page.getByRole('button', { name: 'Hide Actions' }).count();
|
||||
if (showGiveNumbersExists > 0) {
|
||||
await page.getByRole('button', { name: 'Hide Actions' }).click();
|
||||
}
|
||||
|
||||
// Look for the contact by name
|
||||
const contactItems = page.locator('li[data-testid="contactListItem"]');
|
||||
const contactCount = await contactItems.count();
|
||||
|
||||
// Debug: Print all contact names if no match found
|
||||
if (contactCount === 0) {
|
||||
await page.screenshot({ path: 'debug-no-contacts.png' });
|
||||
throw new Error(`No contacts found on page. Screenshot saved as debug-no-contacts.png`);
|
||||
}
|
||||
|
||||
// Check if our contact exists
|
||||
const contactExists = await contactItems.filter({ hasText: contactName }).count();
|
||||
if (contactExists === 0) {
|
||||
// Try alternative selectors
|
||||
const selectors = [
|
||||
'li',
|
||||
'div[data-testid="contactListItem"]',
|
||||
'.contact-item',
|
||||
'[data-testid*="contact"]'
|
||||
];
|
||||
|
||||
for (const selector of selectors) {
|
||||
const testCount = await page.locator(selector).filter({ hasText: contactName }).count();
|
||||
if (testCount > 0) {
|
||||
// Found working selector, use it
|
||||
const contactItem = page.locator(selector).filter({ hasText: contactName }).first();
|
||||
|
||||
// Look for info icon or delete button
|
||||
const infoIconExists = await contactItem.locator('svg.fa-info-circle').count();
|
||||
if (infoIconExists > 0) {
|
||||
await contactItem.locator('svg.fa-info-circle').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Should now be on the contact detail page
|
||||
await expect(page.getByText('Contact Details')).toBeVisible();
|
||||
|
||||
// Look for delete button
|
||||
const deleteButtonExists = await page.getByRole('button', { name: 'Delete Contact' }).count();
|
||||
if (deleteButtonExists > 0) {
|
||||
await page.getByRole('button', { name: 'Delete Contact' }).click();
|
||||
|
||||
// Handle confirmation dialog
|
||||
await expect(page.getByRole('button', { name: 'Yes, Delete' })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Yes, Delete' }).click();
|
||||
|
||||
// Wait for dialog to close
|
||||
await expect(page.getByRole('button', { name: 'Yes, Delete' })).toBeHidden();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Contact "${contactName}" not found on contacts page`);
|
||||
}
|
||||
|
||||
// Use the standard flow
|
||||
const contactItem = contactItems.filter({ hasText: contactName }).first();
|
||||
|
||||
// Look for info icon
|
||||
const infoIconExists = await contactItem.locator('svg.fa-info-circle').count();
|
||||
if (infoIconExists > 0) {
|
||||
await contactItem.locator('svg.fa-info-circle').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Should now be on the contact detail page
|
||||
await expect(page.getByText('Contact Details')).toBeVisible();
|
||||
|
||||
// Look for delete button
|
||||
const deleteButtonExists = await page.getByRole('button', { name: 'Delete Contact' }).count();
|
||||
if (deleteButtonExists > 0) {
|
||||
await page.getByRole('button', { name: 'Delete Contact' }).click();
|
||||
|
||||
// Handle confirmation dialog
|
||||
await expect(page.getByRole('button', { name: 'Yes, Delete' })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Yes, Delete' }).click();
|
||||
|
||||
// Wait for dialog to close
|
||||
await expect(page.getByRole('button', { name: 'Yes, Delete' })).toBeHidden();
|
||||
}
|
||||
}
|
||||
const contactName = createContactName(did);
|
||||
// go to the detail page for this contact
|
||||
await page.locator(`li[data-testid="contactListItem"] h2:has-text("${contactName}") + span svg.fa-circle-info`).click();
|
||||
// delete the 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();
|
||||
}
|
||||
|
||||
export async function generateNewEthrUser(page: Page): Promise<string> {
|
||||
@@ -180,34 +90,14 @@ export async function generateAndRegisterEthrUser(page: Page): Promise<string> {
|
||||
await importUser(page, '000'); // switch to user 000
|
||||
|
||||
await page.goto('./contacts');
|
||||
|
||||
const contactName = createContactName(newDid);
|
||||
|
||||
const contactInput = `${newDid}, ${contactName}`;
|
||||
|
||||
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(contactInput);
|
||||
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(`${newDid}, ${contactName}`);
|
||||
await page.locator('button > svg.fa-plus').click();
|
||||
|
||||
// Wait for the contact to be added first
|
||||
// register them
|
||||
await page.locator('div[role="alert"] button:has-text("Yes")').click();
|
||||
// wait for it to disappear because the next steps may depend on alerts being gone
|
||||
await expect(page.locator('div[role="alert"] button:has-text("Yes")')).toBeHidden();
|
||||
await expect(page.locator('li', { hasText: contactName })).toBeVisible();
|
||||
|
||||
// Wait longer for the registration alert to appear (it has a 1-second timeout)
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Check if the registration alert is present
|
||||
const alertCount = await page.locator('div[role="alert"]').count();
|
||||
|
||||
if (alertCount > 0) {
|
||||
// Check if this is a registration alert (contains "Yes" button)
|
||||
const yesButtonCount = await page.locator('div[role="alert"] button:has-text("Yes")').count();
|
||||
if (yesButtonCount > 0) {
|
||||
// register them
|
||||
await page.locator('div[role="alert"] button:has-text("Yes")').click();
|
||||
|
||||
// wait for it to disappear because the next steps may depend on alerts being gone
|
||||
await expect(page.locator('div[role="alert"] button:has-text("Yes")')).toBeHidden();
|
||||
}
|
||||
}
|
||||
|
||||
return newDid;
|
||||
}
|
||||
@@ -219,7 +109,7 @@ export async function generateRandomString(length: number): Promise<string> {
|
||||
|
||||
// Function to create an array of unique strings
|
||||
export async function createUniqueStringsArray(count: number): Promise<string[]> {
|
||||
const stringsArray: string[] = [];
|
||||
const stringsArray = [];
|
||||
const stringLength = 16;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
@@ -232,7 +122,7 @@ export async function createUniqueStringsArray(count: number): Promise<string[]>
|
||||
|
||||
// Function to create an array of two-digit non-zero numbers
|
||||
export async function createRandomNumbersArray(count: number): Promise<number[]> {
|
||||
const numbersArray: number[] = [];
|
||||
const numbersArray = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
let randomNumber = Math.floor(Math.random() * 99) + 1;
|
||||
|
||||
Reference in New Issue
Block a user