diff --git a/playwright.config-local.ts b/playwright.config-local.ts index c358735e..8a3ba5e7 100644 --- a/playwright.config-local.ts +++ b/playwright.config-local.ts @@ -18,13 +18,20 @@ export default defineConfig({ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, + /* Load the app once before the tests so the dev server is warm; see that file + * for the Firefox WebAssembly failure it reduces. */ + globalSetup: "./test-playwright/warmUpDevServer.ts", + /* Retry once locally: with several instances in parallel, Firefox can fail to + * start its WebAssembly compiler (so the app cannot open its database), at + * any point in a run. A retried pass is reported as "flaky", not hidden. */ + retries: process.env.CI ? 2 : 1, /* Opt out of parallel tests on CI. */ workers: 3, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: [ ['list'], + // explains failures caused by the Firefox WebAssembly start-up error + ['./test-playwright/webAssemblyHintReporter.ts'], ['html'], // ['html', { open: 'never' }], // don't automatically open report page after tests complete even with failures ['json', { outputFile: 'test-playwright-results/test-results.json' }] diff --git a/src/views/ClaimView.vue b/src/views/ClaimView.vue index 5d5c0aea..a80b790f 100644 --- a/src/views/ClaimView.vue +++ b/src/views/ClaimView.vue @@ -774,6 +774,9 @@ export default class ClaimView extends Vue { offerType?: string; } | null = null; detailsForOffer: { fulfillsPlanHandleId?: string } | null = null; + // the claim and its linked offer and project; Affirm Delivery waits for it so + // an early tap does not fall back to the person in place of the project + claimLoad: Promise = Promise.resolve(); // Project information for fulfillsPlanHandleId projectInfo: { name: string; @@ -1003,7 +1006,8 @@ export default class ClaimView extends Vue { const claimId = this.$route.params.id as string; if (claimId) { - await this.loadClaim(claimId, this.activeDid); + this.claimLoad = this.loadClaim(claimId, this.activeDid); + await this.claimLoad; await this.loadClaimNeighbors(); } else { this.notify.error("No claim ID was provided."); @@ -1408,12 +1412,14 @@ export default class ClaimView extends Vue { }; (this.$router as Router).push(route).then(async () => { this.resetThisValues(); - await this.loadClaim(claimId, this.activeDid); + this.claimLoad = this.loadClaim(claimId, this.activeDid); + await this.claimLoad; await this.loadClaimNeighbors(); }); } - handleAffirmDeliveryClick() { + async handleAffirmDeliveryClick() { + await this.claimLoad; if (!this.isRegistered) { this.notify.error("You must be registered to affirm delivery."); return; diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 8ede6cfa..5b548941 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -12,6 +12,7 @@ Raymer * @version 1.0.0 */ id="Content" class="p-6 pb-24 max-w-3xl mx-auto" :data-active-did="activeDid" + :data-new-activity-loaded="newActivityLoaded" > @@ -421,6 +422,8 @@ export default class HomeView extends Vue { isRegistered = false; // what each New Activity list holds that the person hasn't seen newActivityCounts: UnseenCounts | null = null; + // the New Activity load finished (and stored any first start), for tests + newActivityLoaded = false; searchBoxes: Array<{ name: string; bbox: BoundingBox; @@ -526,9 +529,9 @@ export default class HomeView extends Vue { // Settings already loaded in initializeIdentity() // Contacts already loaded in initializeIdentity() // Registration check already handled in initializeIdentity() - await this.loadFeedData(); - - await this.loadNewActivityCounts(); + // in parallel, so a first visit records where New Activity starts even + // when the person leaves before the feed finishes + await Promise.all([this.loadFeedData(), this.loadNewActivityCounts()]); logger.debug("[HomeView] mounted() - component lifecycle completed", { timestamp: new Date().toISOString(), @@ -775,6 +778,7 @@ export default class HomeView extends Vue { private async loadNewActivityCounts() { if (!this.activeDid) { this.newActivityCounts = null; + this.newActivityLoaded = true; return; } try { @@ -807,6 +811,8 @@ export default class HomeView extends Vue { error: errorStringForLog(error), }); this.newActivityCounts = null; + } finally { + this.newActivityLoaded = true; } } diff --git a/src/views/StartView.vue b/src/views/StartView.vue index 595cfd48..e2a3cf7e 100644 --- a/src/views/StartView.vue +++ b/src/views/StartView.vue @@ -32,7 +32,15 @@
-

+

+ This browser could not run WebAssembly, which the app needs to open + its data. Try Again first, since this is sometimes temporary. If it + keeps happening, a privacy or security setting is probably blocking it + (such as Tor Browser's "Safest" level, Apple's Lockdown Mode, or a + strict security mode in Edge): allow this site in that setting, or use + another browser. +

+

The app could not open its data on this device, so nothing else will work until this is fixed. Reinstalling usually does not help, because the phone restores the same data from its backup. @@ -297,6 +305,15 @@ export default class StartView extends Vue { ].join("\n\n"); } + /** + * The web database (sql.js) needs WebAssembly; browsers report its absence + * as, for example, "no WebAssembly compiler available" (Firefox) or + * "WebAssembly is not defined". + */ + get webAssemblyUnavailable(): boolean { + return /WebAssembly/i.test(this.startupError); + } + get supportMailto(): string { // Mail apps truncate long mailto links; Copy Details carries the full text. const body = this.errorReport.slice(0, 1800); diff --git a/test-playwright/55-new-activity.spec.ts b/test-playwright/55-new-activity.spec.ts index 75086434..92ff416d 100644 --- a/test-playwright/55-new-activity.spec.ts +++ b/test-playwright/55-new-activity.spec.ts @@ -1,11 +1,13 @@ import { test, expect } from '@playwright/test'; -import { switchToUser, getTestUserData, importUserFromAccount } from './testUtils'; +import { switchToUser, getTestUserData, importUserFromAccount, waitForNewActivityLoaded } from './testUtils'; test('New offers for another user', async ({ page }) => { await page.goto('./'); // Get the auto-created DID from the HomeView await page.waitForLoadState('networkidle'); + // this first Home visit records where this user's New Activity starts + await waitForNewActivityLoaded(page); const autoCreatedDid = await page.getAttribute('#Content', 'data-active-did'); if (!autoCreatedDid) { diff --git a/test-playwright/testUtils.ts b/test-playwright/testUtils.ts index 9968b0f6..8453bd53 100644 --- a/test-playwright/testUtils.ts +++ b/test-playwright/testUtils.ts @@ -31,11 +31,18 @@ export async function importUserFromAccount(page: Page, id?: string): Promise { + if (!(await switchIdentityLink.isVisible())) { + await page.getByTestId("advancedSettings").click(); + } + await expect(switchIdentityLink).toBeVisible({ timeout: 1000 }); + }).toPass({ timeout: 15000 }); // Use the identity switcher to add User Zero - await page.locator("#switch-identity-link").click(); + await switchIdentityLink.click(); await page.locator("#start-link").click(); // Select "You have a seed" option @@ -356,6 +363,15 @@ export function getPageLoadTimeout(): number { return getAdaptiveTimeout(30000, 1.4); } +/** + * Wait for Home to finish loading New Activity. A first visit for an identity + * records where New Activity starts; leaving before then makes a later visit + * the start, and activity created in between never counts as new. + */ +export async function waitForNewActivityLoaded(page: Page): Promise { + await expect(page.locator('#Content')).toHaveAttribute('data-new-activity-loaded', 'true', { timeout: 15000 }); +} + /** * PHASE 1 FIX: Wait for registration status to settle * @@ -385,6 +401,7 @@ export async function waitForRegistrationStatusToSettle(page: Page): Promise e.message || ""); + for (const attachment of result.attachments) { + if (attachment.name === "error-context" && attachment.path) { + try { + // the page appears under "Page snapshot" or, for a failed expect, + // under "Error details"; skip "Test source", which quotes the spec + const context = readFileSync(attachment.path, "utf8"); + const end = context.indexOf("# Test source"); + texts.push(end >= 0 ? context.slice(0, end) : context); + } catch { + // the snapshot file may be missing; the errors are still checked + } + } + } + if (texts.some((text) => text.includes(MARKER))) { + const name = `${test.parent.project()?.name ?? ""} › ${test.title}${result.retry ? ` (retry ${result.retry})` : ""}`; + this.hits.push(name); + console.log(`\n WebAssembly start-up failure: ${name}\n ${HINT}\n`); + } + } + + onEnd(): void { + if (this.hits.length > 0) { + console.log( + `\n ${this.hits.length} failure(s) were the Firefox WebAssembly start-up error:\n ` + + this.hits.join("\n ") + + `\n ${HINT}\n`, + ); + } + } +}