fix problem with project ID for "given to" project links, fix deep-link redirect for web, update browserslist DB
This commit is contained in:
Generated
+1990
-318
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
|
||||
{
|
||||
"name": "giftopia",
|
||||
"version": "1.4.4",
|
||||
|
||||
@@ -450,7 +450,26 @@ export default class GiftedDialog extends Vue {
|
||||
TIMEOUTS.MODAL,
|
||||
);
|
||||
} else {
|
||||
this.safeNotify.success("That gift was recorded.", TIMEOUTS.VERY_LONG);
|
||||
if (result.embeddedRecordError) {
|
||||
// The claim was stored but the server could not record part of it,
|
||||
// eg. the link to the project this gift came from. Reporting a plain
|
||||
// success here is how such a gift comes to sit on a project page
|
||||
// that never shows it.
|
||||
logger.warn(
|
||||
"Give recorded but part of it was not:",
|
||||
result.embeddedRecordError,
|
||||
);
|
||||
this.safeNotify.warning(
|
||||
"That gift was recorded, but some of it was not: " +
|
||||
result.embeddedRecordError,
|
||||
TIMEOUTS.MODAL,
|
||||
);
|
||||
} else {
|
||||
this.safeNotify.success(
|
||||
"That gift was recorded.",
|
||||
TIMEOUTS.VERY_LONG,
|
||||
);
|
||||
}
|
||||
|
||||
// Show seed phrase backup reminder if needed
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @fileoverview Handle ID string handling for the Endorser server
|
||||
*
|
||||
* A handle ID is the permanent ID of a claim's entity on Endorser, eg.
|
||||
* "https://endorser.ch/entity/01D25AVGQG1N8E9JNGK7C7DZRD". Deep links carry it
|
||||
* with the prefix stripped off, so the two forms both circulate in the app and
|
||||
* have to be converted deliberately at the boundary between them.
|
||||
*
|
||||
* This module holds no dependencies on purpose. Everything here is string
|
||||
* handling that claims and report queries depend on, so it stays loadable
|
||||
* without a database, a network client, or a build-time environment.
|
||||
*
|
||||
* @module endorserHandleIds
|
||||
*/
|
||||
|
||||
/**
|
||||
* The prefix for handle IDs, the permanent ID for claims on Endorser
|
||||
* @constant {string}
|
||||
*/
|
||||
export const ENDORSER_CH_HANDLE_PREFIX = "https://endorser.ch/entity/";
|
||||
|
||||
/**
|
||||
* from https://tools.ietf.org/html/rfc3986#section-3
|
||||
*
|
||||
* An ID carrying a scheme belongs to whoever issued it; one without a scheme
|
||||
* names an entity on the Endorser server. (libs/util.ts exports the same
|
||||
* predicate as isGlobalUri for view code.)
|
||||
*/
|
||||
const hasUriScheme = (id: string) => /^[A-Za-z][A-Za-z0-9+.-]+:/.test(id);
|
||||
|
||||
/**
|
||||
* Shorten a full handle ID to the part a deep link carries.
|
||||
*/
|
||||
export function stripEndorserPrefix(claimId: string) {
|
||||
if (claimId && claimId.startsWith(ENDORSER_CH_HANDLE_PREFIX)) {
|
||||
return claimId.substring(ENDORSER_CH_HANDLE_PREFIX.length);
|
||||
} else {
|
||||
return claimId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The inverse of stripEndorserPrefix: expand an ID that names an entity on the
|
||||
* Endorser server into the full handle ID.
|
||||
*
|
||||
* An ID taken from a route parameter goes through here before it reaches a
|
||||
* claim or a report query. A claim is a credential that outlives this app and
|
||||
* travels beyond this server, where "01D25AVGQG1N8E9JNGK7C7DZRD" identifies
|
||||
* nothing; the report endpoints match on the full handle.
|
||||
*
|
||||
* An ID that already carries a URI scheme -- a full handle, or a handle from
|
||||
* another system -- is returned unchanged.
|
||||
*/
|
||||
export function addEndorserPrefix(claimId: string) {
|
||||
if (claimId && !hasUriScheme(claimId)) {
|
||||
return ENDORSER_CH_HANDLE_PREFIX + claimId;
|
||||
} else {
|
||||
return claimId;
|
||||
}
|
||||
}
|
||||
+18
-17
@@ -104,11 +104,14 @@ export const CONTACT_URL_PATH_ENDORSER_CH_OLD = "/contact?jwt=";
|
||||
*/
|
||||
export const CONTACT_CONFIRM_URL_PATH_TIME_SAFARI = "/contact/confirm/";
|
||||
|
||||
/**
|
||||
* The prefix for handle IDs, the permanent ID for claims on Endorser
|
||||
* @constant {string}
|
||||
*/
|
||||
export const ENDORSER_CH_HANDLE_PREFIX = "https://endorser.ch/entity/";
|
||||
// Handle ID string handling lives in its own module so that it can be loaded
|
||||
// and tested without everything this one needs. Re-exported here because that
|
||||
// is where callers have always found it.
|
||||
export {
|
||||
ENDORSER_CH_HANDLE_PREFIX,
|
||||
addEndorserPrefix,
|
||||
stripEndorserPrefix,
|
||||
} from "./endorserHandleIds";
|
||||
|
||||
export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCredential> =
|
||||
{
|
||||
@@ -222,14 +225,6 @@ export const containsNonHiddenDid = (obj: any) => {
|
||||
return testRecursivelyOnStrings(obj, (s: any) => isDid(s) && !isHiddenDid(s));
|
||||
};
|
||||
|
||||
export function stripEndorserPrefix(claimId: string) {
|
||||
if (claimId && claimId.startsWith(ENDORSER_CH_HANDLE_PREFIX)) {
|
||||
return claimId.substring(ENDORSER_CH_HANDLE_PREFIX.length);
|
||||
} else {
|
||||
return claimId;
|
||||
}
|
||||
}
|
||||
|
||||
// similar logic is found in endorser-mobile
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function removeSchemaContext(obj: any) {
|
||||
@@ -1226,19 +1221,25 @@ export async function createAndSubmitClaim(
|
||||
},
|
||||
});
|
||||
|
||||
// The server wraps its result: { success: { claimId, handleId, ... } }.
|
||||
// Reading the top level instead leaves every field undefined, which hides
|
||||
// embeddedRecordError -- the server's way of saying it stored the claim but
|
||||
// could not record part of it, eg. a link to a project.
|
||||
const result = response.data?.success;
|
||||
|
||||
logger.debug("[Claim Submission] ✅ Claim submitted successfully:", {
|
||||
requestId,
|
||||
status: response.status,
|
||||
handleId: response.data?.handleId,
|
||||
handleId: result?.handleId,
|
||||
responseSize: JSON.stringify(response.data).length,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
claimId: response.data?.claimId,
|
||||
handleId: response.data?.handleId,
|
||||
embeddedRecordError: response.data?.embeddedRecordError,
|
||||
claimId: result?.claimId,
|
||||
handleId: result?.handleId,
|
||||
embeddedRecordError: result?.embeddedRecordError,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
// Enhanced error logging with comprehensive context
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
addEndorserPrefix,
|
||||
stripEndorserPrefix,
|
||||
ENDORSER_CH_HANDLE_PREFIX,
|
||||
} from "@/libs/endorserHandleIds";
|
||||
|
||||
/**
|
||||
* A project reached through a deep link arrives with its handle shortened, and
|
||||
* whatever the app holds at that moment is what it signs into a claim and sends
|
||||
* to the report endpoints. A short ID in either place is a link to nothing.
|
||||
*/
|
||||
|
||||
const SHORT_ID = "01D25AVGQG1N8E9JNGK7C7DZRD";
|
||||
const FULL_ID = ENDORSER_CH_HANDLE_PREFIX + SHORT_ID;
|
||||
|
||||
describe("addEndorserPrefix", () => {
|
||||
it("expands an ID that names an entity on this server", () => {
|
||||
expect(addEndorserPrefix(SHORT_ID)).toEqual(FULL_ID);
|
||||
});
|
||||
|
||||
it("leaves a full handle alone", () => {
|
||||
expect(addEndorserPrefix(FULL_ID)).toEqual(FULL_ID);
|
||||
});
|
||||
|
||||
it("leaves a handle from another system alone", () => {
|
||||
// Anything carrying a URI scheme belongs to whoever issued it
|
||||
expect(addEndorserPrefix("external:some-project")).toEqual(
|
||||
"external:some-project",
|
||||
);
|
||||
expect(addEndorserPrefix("https://other.example/entity/abc")).toEqual(
|
||||
"https://other.example/entity/abc",
|
||||
);
|
||||
expect(addEndorserPrefix("did:ethr:0x0000")).toEqual("did:ethr:0x0000");
|
||||
});
|
||||
|
||||
it("passes an empty ID through rather than making a prefix out of it", () => {
|
||||
expect(addEndorserPrefix("")).toEqual("");
|
||||
});
|
||||
|
||||
it("is the inverse of stripEndorserPrefix", () => {
|
||||
expect(addEndorserPrefix(stripEndorserPrefix(FULL_ID))).toEqual(FULL_ID);
|
||||
expect(stripEndorserPrefix(addEndorserPrefix(SHORT_ID))).toEqual(SHORT_ID);
|
||||
});
|
||||
|
||||
it("is idempotent, so an ID may pass through more than one caller", () => {
|
||||
expect(addEndorserPrefix(addEndorserPrefix(SHORT_ID))).toEqual(FULL_ID);
|
||||
});
|
||||
});
|
||||
@@ -19,13 +19,19 @@
|
||||
: "Opening on your Android device..."
|
||||
}}
|
||||
</p>
|
||||
<p v-else>Opening the app...</p>
|
||||
<p v-else>Opening the page...</p>
|
||||
<p class="text-sm mt-2">
|
||||
<span v-if="isMobile"
|
||||
>If the app doesn't open automatically, use one of these
|
||||
options:</span
|
||||
>
|
||||
<span v-else>Choose how you'd like to open this link:</span>
|
||||
<!-- Not an offer of a choice: openDeepLink has already sent a
|
||||
desktop browser to the web page, so anyone reading this is
|
||||
here because that did not happen. -->
|
||||
<span v-else
|
||||
>If the page doesn't open automatically, use one of these
|
||||
options:</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -148,6 +154,14 @@ export default class DeepLinkRedirectView extends Vue {
|
||||
this.openDeepLink();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the browser on to the link's destination.
|
||||
*
|
||||
* A desktop browser goes straight to the web page, so the buttons this view
|
||||
* renders are only ever seen there when this fails. A mobile browser gets the
|
||||
* custom scheme, which leaves the page loaded whether or not an app answers,
|
||||
* which is what the buttons below are really for.
|
||||
*/
|
||||
private openDeepLink() {
|
||||
if (!this.deepLinkUrl || !this.webUrl) {
|
||||
this.pageError =
|
||||
|
||||
@@ -246,7 +246,7 @@
|
||||
<!-- Offers & Gifts to & from this -->
|
||||
<div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
|
||||
<!-- First, offers on the left-->
|
||||
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
||||
<div class="bg-slate-100 px-4 py-3 rounded-md" data-testId="offers-to">
|
||||
<div v-if="activeDid && isRegistered" class="mb-4">
|
||||
<div class="text-center">
|
||||
<button
|
||||
@@ -818,9 +818,15 @@ export default class ProjectViewView extends Vue {
|
||||
// this id to their dialogs. Leaving it empty across an await lets a click
|
||||
// land in that window and record an offer with no project attached, which
|
||||
// the server then files against the user instead.
|
||||
// A deep link carries the ID with the Endorser prefix stripped off, so
|
||||
// expand it here rather than letting the short form reach a claim. The
|
||||
// server's own handle replaces this as soon as loadProject returns; this
|
||||
// covers the window before that, for the same reason as above.
|
||||
const pathParam = window.location.pathname.substring("/project/".length);
|
||||
if (pathParam) {
|
||||
this.projectId = decodeURIComponent(pathParam);
|
||||
this.projectId = serverUtil.addEndorserPrefix(
|
||||
decodeURIComponent(pathParam),
|
||||
);
|
||||
}
|
||||
|
||||
const settings = await this.$accountSettings();
|
||||
@@ -904,6 +910,10 @@ export default class ProjectViewView extends Vue {
|
||||
try {
|
||||
const resp = await this.axios.get(url, { headers });
|
||||
if (resp.status === 200) {
|
||||
// Take the handle the server reports for this project. The prefix is
|
||||
// that deployment's configuration rather than something this app can
|
||||
// know, and this is the exact string the report endpoints match on.
|
||||
this.projectId = resp.data.handleId || this.projectId;
|
||||
const startTime = resp.data.claim?.startTime;
|
||||
if (startTime != null) {
|
||||
const startDateTime = new Date(startTime);
|
||||
|
||||
@@ -79,7 +79,9 @@ test('Record item given from image-share', async ({ page }) => {
|
||||
// on shared photo page, choose the gift option
|
||||
await page.getByRole('button').filter({ hasText: /gift/i }).click();
|
||||
|
||||
await page.getByTestId('imagery').getByRole('img').isVisible();
|
||||
// (isVisible() answered immediately and the answer went nowhere, so the
|
||||
// shared image was never actually checked.)
|
||||
await expect(page.getByTestId('imagery').getByRole('img')).toBeVisible();
|
||||
await page.getByPlaceholder('What was received').fill(finalTitle);
|
||||
await page.getByRole('spinbutton').fill('2');
|
||||
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||
|
||||
@@ -34,12 +34,14 @@ async function testProjectGive(page: Page, isToProject: boolean) {
|
||||
|
||||
// refresh the page
|
||||
await page.reload();
|
||||
// check that the give is in the list
|
||||
await page
|
||||
.getByTestId(selector)
|
||||
.locator('div ul li:first-child')
|
||||
.filter({ hasText: finalTitle })
|
||||
.isVisible();
|
||||
// Check that the give is in the list.
|
||||
// (The line that used to sit here called isVisible() and threw the answer
|
||||
// away, so it passed whether or not the give showed. It also looked under
|
||||
// 'div ul li', which gives-from has no room for: its ul sits directly under
|
||||
// the test id, while gives-to wraps its own in a div.)
|
||||
await expect(
|
||||
page.getByTestId(selector).locator('li').filter({ hasText: finalTitle }),
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
test('Record a give to a project', async ({ page }) => {
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
import { importUser } from './testUtils';
|
||||
|
||||
/**
|
||||
* A project reached through a shared link arrives with its handle shortened:
|
||||
* the link carries "01ABC..." where a project opened from a list carries
|
||||
* "https://endorser.ch/entity/01ABC...". Whatever the page holds at that moment
|
||||
* is what it signs into every claim made from it, and the server matches the
|
||||
* full handle, so a short ID leaves a claim recorded and attached to nothing:
|
||||
* the gift is accepted, reports a success, and never appears on the project.
|
||||
*
|
||||
* Spec 37 records the same gifts on a project opened from the discover list,
|
||||
* where the handle is already full. Only the way in differs here, and that is
|
||||
* the whole point: these tests would have passed while the shared-link route
|
||||
* was broken, so they are worth their own file rather than a parameter on 37.
|
||||
*/
|
||||
|
||||
/** The prefix the server puts in front of an entity's ID to make its handle. */
|
||||
const ENTITY_HANDLE_PREFIX = 'https://endorser.ch/entity/';
|
||||
|
||||
/**
|
||||
* Open a project from the discover list and return its ID in both forms: the
|
||||
* full handle the app holds when it came from a list, and the short form a
|
||||
* shared link carries.
|
||||
*/
|
||||
async function findProjectIds(
|
||||
page: Page,
|
||||
): Promise<{ fullId: string; shortId: string }> {
|
||||
await page.goto('./discover');
|
||||
await page.getByTestId('closeOnboardingAndFinish').click();
|
||||
await page.locator('ul#listDiscoverResults li:first-child a').click();
|
||||
await page.waitForURL(/\/project\//);
|
||||
|
||||
const fullId = decodeURIComponent(
|
||||
new URL(page.url()).pathname.substring('/project/'.length),
|
||||
);
|
||||
expect(fullId).toContain(ENTITY_HANDLE_PREFIX);
|
||||
|
||||
return { fullId, shortId: fullId.substring(ENTITY_HANDLE_PREFIX.length) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a project the way a shared link does, so the page holds only the short
|
||||
* ID. The redirect page sends a desktop browser straight on to the project,
|
||||
* which is the same place the native handler routes to.
|
||||
*/
|
||||
async function openProjectByDeepLink(page: Page, shortId: string) {
|
||||
await page.goto('./deep-link/project/' + shortId);
|
||||
// The whole pathname, because "/deep-link/project/<id>" also ends in
|
||||
// "/project/<id>" and a looser wait returns before the redirect happens.
|
||||
await page.waitForURL((url) => url.pathname === '/project/' + shortId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a gift on the open project and check that the project shows it.
|
||||
*
|
||||
* @param isToProject true gives to the project, false gives from it. The two
|
||||
* travel in different fields of the claim -- what the gift fulfills, and who
|
||||
* provided it -- so a short ID can break one and leave the other working.
|
||||
*/
|
||||
async function recordGiftOnOpenProject(page: Page, isToProject: boolean) {
|
||||
const selector = isToProject ? 'gives-to' : 'gives-from';
|
||||
const finalTitle = 'Gift ' + Math.random().toString(36).substring(2, 6);
|
||||
const randomNonZeroNumber = Math.floor(Math.random() * 99) + 1;
|
||||
|
||||
await page.getByTestId(selector).locator('div:first-child div button').click();
|
||||
await page.getByPlaceholder('What was given').fill(finalTitle);
|
||||
await page.getByRole('spinbutton').fill(randomNonZeroNumber.toString());
|
||||
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||
|
||||
// Anything short of a plain success means the server could not record part
|
||||
// of the claim, which is exactly how an unlinked gift used to be reported.
|
||||
await expect(page.getByText('That gift was recorded.')).toBeVisible();
|
||||
await page.locator('div[role="alert"] button > svg.fa-xmark').click();
|
||||
|
||||
// The list is built from a report query that matches the project's full
|
||||
// handle, so this is the assertion that the claim was linked and not merely
|
||||
// stored.
|
||||
// ("li" rather than "div ul li": the two sections nest their lists
|
||||
// differently, and gives-from puts its ul directly under the test id.)
|
||||
await page.reload();
|
||||
await expect(
|
||||
page.getByTestId(selector).locator('li').filter({ hasText: finalTitle }),
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
test('Record a give from a project opened by its shared-link ID', async ({
|
||||
page,
|
||||
}) => {
|
||||
test.setTimeout(60000);
|
||||
await importUser(page, '00');
|
||||
const { shortId } = await findProjectIds(page);
|
||||
await openProjectByDeepLink(page, shortId);
|
||||
await recordGiftOnOpenProject(page, false);
|
||||
});
|
||||
|
||||
test('Record a give to a project opened by its shared-link ID', async ({
|
||||
page,
|
||||
}) => {
|
||||
test.setTimeout(60000);
|
||||
await importUser(page, '00');
|
||||
const { shortId } = await findProjectIds(page);
|
||||
await openProjectByDeepLink(page, shortId);
|
||||
await recordGiftOnOpenProject(page, true);
|
||||
});
|
||||
|
||||
test('Record an offer on a project opened by its shared-link ID', async ({
|
||||
page,
|
||||
}) => {
|
||||
test.setTimeout(60000);
|
||||
const description = `Offering of ${Math.random().toString(36).substring(2, 5)}`;
|
||||
|
||||
await importUser(page, '00');
|
||||
const { shortId } = await findProjectIds(page);
|
||||
await openProjectByDeepLink(page, shortId);
|
||||
|
||||
// An offer carries the project in its own field, and the buttons that open
|
||||
// these dialogs render before the project finishes loading, so an offer made
|
||||
// straight after landing is the case most likely to escape with a short ID.
|
||||
await expect(page.getByTestId('offerButton')).toBeVisible();
|
||||
await page.getByTestId('offerButton').click();
|
||||
await page.getByTestId('inputDescription').fill(description);
|
||||
await page
|
||||
.getByTestId('inputOfferAmount')
|
||||
.locator('input')
|
||||
.fill(String(Math.floor(Math.random() * 998) + 1));
|
||||
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||
await page.getByRole('alert').filter({ hasText: 'Success' }).getByRole('button').click();
|
||||
|
||||
// The project's own offer list comes from a report query on the plan, so the
|
||||
// description showing up here says the project reference survived. Reading
|
||||
// the offer's claim instead would prove nothing: the claim holds whatever was
|
||||
// signed, linked or not.
|
||||
await page.reload();
|
||||
await expect(
|
||||
page.getByTestId('offers-to').locator('li').filter({ hasText: description }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('A shared project link lands on the project holding only the short ID', async ({
|
||||
page,
|
||||
}) => {
|
||||
test.setTimeout(60000);
|
||||
await importUser(page, '00');
|
||||
const { fullId, shortId } = await findProjectIds(page);
|
||||
|
||||
await page.goto('./deep-link/project/' + shortId);
|
||||
await page.waitForURL((url) => url.pathname.startsWith('/project/'));
|
||||
|
||||
// This is where the short ID comes from, and the condition the three tests
|
||||
// above depend on. If shared links ever carry the full handle instead, this
|
||||
// is the test that should fail first, because those three would still pass
|
||||
// while no longer describing anything real.
|
||||
const landedId = decodeURIComponent(
|
||||
new URL(page.url()).pathname.substring('/project/'.length),
|
||||
);
|
||||
expect(landedId).toEqual(shortId);
|
||||
expect(landedId).not.toEqual(fullId);
|
||||
|
||||
// The project still has to load from that short ID, or the page is useless
|
||||
// whatever it does with claims.
|
||||
await expect(page.getByTestId('offerButton')).toBeVisible();
|
||||
});
|
||||
@@ -232,7 +232,11 @@ test('Add contact, copy details, delete, and import from paste & from file', asy
|
||||
|
||||
// see contact details on the second contact
|
||||
await page.getByTestId('contactListItem').nth(1).locator('h2 > a').click();
|
||||
await page.getByRole('heading', { name: 'Identifier Details' }).isVisible();
|
||||
// (isVisible() answered immediately and the answer went nowhere, so this
|
||||
// neither waited for the page nor checked it.)
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Identifier Details' }),
|
||||
).toBeVisible();
|
||||
// remove contact
|
||||
await page.locator('button > svg.fa-trash-can').click();
|
||||
await page.locator('div[role="alert"] button:has-text("Yes")').click();
|
||||
|
||||
@@ -110,12 +110,21 @@ export async function switchToUser(page: Page, did: string): Promise<void> {
|
||||
await switchIdentityLink.click();
|
||||
}
|
||||
|
||||
const didElem = await page.locator(`code:has-text("${did}")`);
|
||||
await didElem.isVisible();
|
||||
// (A locator is not a promise, so awaiting one does nothing, and isVisible()
|
||||
// answers immediately instead of waiting. Both lines that used to sit here
|
||||
// looked like waits and were not; expect() is what actually polls.)
|
||||
const didElem = page.locator(`code:has-text("${did}")`);
|
||||
await expect(didElem).toBeVisible();
|
||||
await didElem.click();
|
||||
|
||||
// wait for the switch to happen and the account page to fully load
|
||||
await page.getByTestId("didWrapper").locator('code:has-text("did:")');
|
||||
// Confirm the switch took, rather than returning while the app is still on
|
||||
// whatever page the click landed on. (The line that used to sit here waited
|
||||
// for the account page's DID without going back to the account page, so it
|
||||
// could only ever have passed by accident; it never ran as a check at all.
|
||||
// Every caller navigates straight afterwards, so ending up here costs them
|
||||
// nothing.)
|
||||
await page.goto("./account");
|
||||
await expect(page.getByTestId("didWrapper").locator("code")).toHaveText(did);
|
||||
}
|
||||
|
||||
export function createContactName(did: string): string {
|
||||
|
||||
Reference in New Issue
Block a user