test fixes (mostly intermittent ones)
This commit is contained in:
@@ -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' }]
|
||||
|
||||
@@ -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<void> = 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;
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<TopMessage />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-1
@@ -32,7 +32,15 @@
|
||||
<!-- Startup error: recovery options instead of identity creation -->
|
||||
<div v-if="startupError" class="max-w-3xl mx-auto">
|
||||
<div class="p-4 bg-red-50 border border-red-300 rounded-lg">
|
||||
<p class="text-red-800">
|
||||
<p v-if="webAssemblyUnavailable" class="text-red-800">
|
||||
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.
|
||||
</p>
|
||||
<p v-else class="text-red-800">
|
||||
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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -31,11 +31,18 @@ export async function importUserFromAccount(page: Page, id?: string): Promise<st
|
||||
// Navigate to AccountViewView to use the Identity Switcher
|
||||
await page.goto("./account");
|
||||
|
||||
// Click "Show Advanced Settings" to reveal the identity switcher
|
||||
// Click "Show Advanced Settings" to reveal the identity switcher; loading the
|
||||
// settings resets that section to closed, so retry a click that lands first
|
||||
const switchIdentityLink = page.locator("#switch-identity-link");
|
||||
await expect(async () => {
|
||||
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<void> {
|
||||
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<voi
|
||||
// Navigate to home to trigger the registration check
|
||||
await page.goto('./');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await waitForNewActivityLoaded(page);
|
||||
|
||||
// Wait for the registration check to complete by monitoring the usage limits section
|
||||
// This ensures the async registration check has finished
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { chromium, FullConfig } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Loads the app once before any test so Vite has bundled its dependencies and
|
||||
* transformed the modules (including the SQL worker) before the workers start.
|
||||
*
|
||||
* Reason: with several Firefox instances running in parallel, Firefox can fail
|
||||
* with "no WebAssembly compiler available", sql.js cannot start, and the app
|
||||
* shows "Time Safari Cannot Start". It is most frequent at the start of a run
|
||||
* against a cold dev server, which this avoids; it also happens mid-run, which
|
||||
* this does not, so the retry in playwright.config-local.ts stays.
|
||||
*/
|
||||
export default async function warmUpDevServer(config: FullConfig) {
|
||||
const baseURL = config.projects[0]?.use?.baseURL || "http://localhost:8080";
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await page.goto(baseURL, { timeout: 120000 });
|
||||
// Vite reloads the page if it finds dependencies it has not bundled yet,
|
||||
// so wait for the app to render and the network to go quiet afterward.
|
||||
await page.locator("#Content").first().waitFor({ timeout: 120000 });
|
||||
await page.waitForLoadState("networkidle", { timeout: 120000 });
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { readFileSync } from "fs";
|
||||
import type {
|
||||
Reporter,
|
||||
TestCase,
|
||||
TestResult,
|
||||
} from "@playwright/test/reporter";
|
||||
|
||||
/**
|
||||
* Explains failures where the app could not start because Firefox reported
|
||||
* "no WebAssembly compiler available" (see warmUpDevServer.ts). The error
|
||||
* shows only in the page snapshot, since the test itself times out waiting
|
||||
* for an element on a page that never loaded.
|
||||
*/
|
||||
const MARKER = "no WebAssembly compiler available";
|
||||
|
||||
const HINT = [
|
||||
`The app could not start: Firefox reported "${MARKER}", so sql.js could not open the database.`,
|
||||
"Previous hypothesis was a cold dev server while several Firefox instances start, so warmUpDevServer.ts",
|
||||
"loads the app before the tests; it is not a definitive fix (this also happens mid-run), so you may",
|
||||
"want to look further. The local retry in playwright.config-local.ts usually passes on the second try.",
|
||||
].join("\n ");
|
||||
|
||||
export default class WebAssemblyHintReporter implements Reporter {
|
||||
private hits: string[] = [];
|
||||
|
||||
printsToStdio(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
onTestEnd(test: TestCase, result: TestResult): void {
|
||||
if (result.status === "passed" || result.status === "skipped") {
|
||||
return;
|
||||
}
|
||||
const texts = result.errors.map((e) => 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`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user