test fixes (mostly intermittent ones)
This commit is contained in:
@@ -18,13 +18,20 @@ export default defineConfig({
|
|||||||
fullyParallel: true,
|
fullyParallel: true,
|
||||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
/* Retry on CI only */
|
/* Load the app once before the tests so the dev server is warm; see that file
|
||||||
retries: process.env.CI ? 2 : 0,
|
* 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. */
|
/* Opt out of parallel tests on CI. */
|
||||||
workers: 3,
|
workers: 3,
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
reporter: [
|
reporter: [
|
||||||
['list'],
|
['list'],
|
||||||
|
// explains failures caused by the Firefox WebAssembly start-up error
|
||||||
|
['./test-playwright/webAssemblyHintReporter.ts'],
|
||||||
['html'],
|
['html'],
|
||||||
// ['html', { open: 'never' }], // don't automatically open report page after tests complete even with failures
|
// ['html', { open: 'never' }], // don't automatically open report page after tests complete even with failures
|
||||||
['json', { outputFile: 'test-playwright-results/test-results.json' }]
|
['json', { outputFile: 'test-playwright-results/test-results.json' }]
|
||||||
|
|||||||
@@ -774,6 +774,9 @@ export default class ClaimView extends Vue {
|
|||||||
offerType?: string;
|
offerType?: string;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
detailsForOffer: { fulfillsPlanHandleId?: 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
|
// Project information for fulfillsPlanHandleId
|
||||||
projectInfo: {
|
projectInfo: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -1003,7 +1006,8 @@ export default class ClaimView extends Vue {
|
|||||||
|
|
||||||
const claimId = this.$route.params.id as string;
|
const claimId = this.$route.params.id as string;
|
||||||
if (claimId) {
|
if (claimId) {
|
||||||
await this.loadClaim(claimId, this.activeDid);
|
this.claimLoad = this.loadClaim(claimId, this.activeDid);
|
||||||
|
await this.claimLoad;
|
||||||
await this.loadClaimNeighbors();
|
await this.loadClaimNeighbors();
|
||||||
} else {
|
} else {
|
||||||
this.notify.error("No claim ID was provided.");
|
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.$router as Router).push(route).then(async () => {
|
||||||
this.resetThisValues();
|
this.resetThisValues();
|
||||||
await this.loadClaim(claimId, this.activeDid);
|
this.claimLoad = this.loadClaim(claimId, this.activeDid);
|
||||||
|
await this.claimLoad;
|
||||||
await this.loadClaimNeighbors();
|
await this.loadClaimNeighbors();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAffirmDeliveryClick() {
|
async handleAffirmDeliveryClick() {
|
||||||
|
await this.claimLoad;
|
||||||
if (!this.isRegistered) {
|
if (!this.isRegistered) {
|
||||||
this.notify.error("You must be registered to affirm delivery.");
|
this.notify.error("You must be registered to affirm delivery.");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ Raymer * @version 1.0.0 */
|
|||||||
id="Content"
|
id="Content"
|
||||||
class="p-6 pb-24 max-w-3xl mx-auto"
|
class="p-6 pb-24 max-w-3xl mx-auto"
|
||||||
:data-active-did="activeDid"
|
:data-active-did="activeDid"
|
||||||
|
:data-new-activity-loaded="newActivityLoaded"
|
||||||
>
|
>
|
||||||
<TopMessage />
|
<TopMessage />
|
||||||
|
|
||||||
@@ -421,6 +422,8 @@ export default class HomeView extends Vue {
|
|||||||
isRegistered = false;
|
isRegistered = false;
|
||||||
// what each New Activity list holds that the person hasn't seen
|
// what each New Activity list holds that the person hasn't seen
|
||||||
newActivityCounts: UnseenCounts | null = null;
|
newActivityCounts: UnseenCounts | null = null;
|
||||||
|
// the New Activity load finished (and stored any first start), for tests
|
||||||
|
newActivityLoaded = false;
|
||||||
searchBoxes: Array<{
|
searchBoxes: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
bbox: BoundingBox;
|
bbox: BoundingBox;
|
||||||
@@ -526,9 +529,9 @@ export default class HomeView extends Vue {
|
|||||||
// Settings already loaded in initializeIdentity()
|
// Settings already loaded in initializeIdentity()
|
||||||
// Contacts already loaded in initializeIdentity()
|
// Contacts already loaded in initializeIdentity()
|
||||||
// Registration check already handled in initializeIdentity()
|
// Registration check already handled in initializeIdentity()
|
||||||
await this.loadFeedData();
|
// in parallel, so a first visit records where New Activity starts even
|
||||||
|
// when the person leaves before the feed finishes
|
||||||
await this.loadNewActivityCounts();
|
await Promise.all([this.loadFeedData(), this.loadNewActivityCounts()]);
|
||||||
|
|
||||||
logger.debug("[HomeView] mounted() - component lifecycle completed", {
|
logger.debug("[HomeView] mounted() - component lifecycle completed", {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
@@ -775,6 +778,7 @@ export default class HomeView extends Vue {
|
|||||||
private async loadNewActivityCounts() {
|
private async loadNewActivityCounts() {
|
||||||
if (!this.activeDid) {
|
if (!this.activeDid) {
|
||||||
this.newActivityCounts = null;
|
this.newActivityCounts = null;
|
||||||
|
this.newActivityLoaded = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -807,6 +811,8 @@ export default class HomeView extends Vue {
|
|||||||
error: errorStringForLog(error),
|
error: errorStringForLog(error),
|
||||||
});
|
});
|
||||||
this.newActivityCounts = null;
|
this.newActivityCounts = null;
|
||||||
|
} finally {
|
||||||
|
this.newActivityLoaded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-1
@@ -32,7 +32,15 @@
|
|||||||
<!-- Startup error: recovery options instead of identity creation -->
|
<!-- Startup error: recovery options instead of identity creation -->
|
||||||
<div v-if="startupError" class="max-w-3xl mx-auto">
|
<div v-if="startupError" class="max-w-3xl mx-auto">
|
||||||
<div class="p-4 bg-red-50 border border-red-300 rounded-lg">
|
<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
|
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
|
work until this is fixed. Reinstalling usually does not help, because
|
||||||
the phone restores the same data from its backup.
|
the phone restores the same data from its backup.
|
||||||
@@ -297,6 +305,15 @@ export default class StartView extends Vue {
|
|||||||
].join("\n\n");
|
].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 {
|
get supportMailto(): string {
|
||||||
// Mail apps truncate long mailto links; Copy Details carries the full text.
|
// Mail apps truncate long mailto links; Copy Details carries the full text.
|
||||||
const body = this.errorReport.slice(0, 1800);
|
const body = this.errorReport.slice(0, 1800);
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
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 }) => {
|
test('New offers for another user', async ({ page }) => {
|
||||||
await page.goto('./');
|
await page.goto('./');
|
||||||
|
|
||||||
// Get the auto-created DID from the HomeView
|
// Get the auto-created DID from the HomeView
|
||||||
await page.waitForLoadState('networkidle');
|
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');
|
const autoCreatedDid = await page.getAttribute('#Content', 'data-active-did');
|
||||||
|
|
||||||
if (!autoCreatedDid) {
|
if (!autoCreatedDid) {
|
||||||
|
|||||||
@@ -31,11 +31,18 @@ export async function importUserFromAccount(page: Page, id?: string): Promise<st
|
|||||||
// Navigate to AccountViewView to use the Identity Switcher
|
// Navigate to AccountViewView to use the Identity Switcher
|
||||||
await page.goto("./account");
|
await page.goto("./account");
|
||||||
|
|
||||||
// Click "Show Advanced Settings" to reveal the identity switcher
|
// Click "Show Advanced Settings" to reveal the identity switcher; loading the
|
||||||
await page.getByTestId("advancedSettings").click();
|
// 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
|
// Use the identity switcher to add User Zero
|
||||||
await page.locator("#switch-identity-link").click();
|
await switchIdentityLink.click();
|
||||||
await page.locator("#start-link").click();
|
await page.locator("#start-link").click();
|
||||||
|
|
||||||
// Select "You have a seed" option
|
// Select "You have a seed" option
|
||||||
@@ -356,6 +363,15 @@ export function getPageLoadTimeout(): number {
|
|||||||
return getAdaptiveTimeout(30000, 1.4);
|
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
|
* 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
|
// Navigate to home to trigger the registration check
|
||||||
await page.goto('./');
|
await page.goto('./');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
|
await waitForNewActivityLoaded(page);
|
||||||
|
|
||||||
// Wait for the registration check to complete by monitoring the usage limits section
|
// Wait for the registration check to complete by monitoring the usage limits section
|
||||||
// This ensures the async registration check has finished
|
// 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