add some other deep links for useful pages, and fix a UI test

This commit is contained in:
2026-09-20 15:17:58 -06:00
parent 7e3f2aa004
commit e350feb132
9 changed files with 125 additions and 79 deletions
+23
View File
@@ -127,9 +127,32 @@ All deep links follow the format: `timesafari://<route>/<param>?<query>`
### App Routes
These take no parameters.
- `timesafari://` — no route at all opens the app at the home feed.
- `timesafari://account`
- `timesafari://discover`
- Query params, all optional:
- `searchText`: prefills the search box
- `searchPeople`: any value switches to the people tab
- `hideOnboarding`: "true" suppresses the onboarding prompt
- `timesafari://invite-one`
- `timesafari://new-activity`
- `timesafari://onboard-meeting-list`
- `timesafari://projects`
- `timesafari://recent-offers-to-user`
- `timesafari://recent-offers-to-user-projects`
- `timesafari://search-area`
- `timesafari://share-my-contact-info`
- `timesafari://statistics`
### Help Routes
- `timesafari://help`
- `timesafari://help-notifications`
- `timesafari://help-notification-types`
- `timesafari://help-onboarding`
- `timesafari://help-terms`
### Claim Routes
+3
View File
@@ -1,6 +1,9 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
// Unit tests live under src/. test-playwright/ holds Playwright specs,
// which use a different runner and fail if Jest collects them.
roots: ['<rootDir>/src'],
moduleFileExtensions: ['ts', 'js', 'json', 'vue'],
transform: {
'^.+\\.ts$': 'ts-jest'
+2 -1
View File
@@ -14,9 +14,10 @@
"type-check:vue": "vue-tsc --noEmit",
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js",
"test:prerequisites": "node scripts/check-prerequisites.js",
"test:unit": "jest",
"check:dependencies": "./scripts/check-dependencies.sh",
"deps:update-daily-notification-plugin": "npm install @timesafari/daily-notification-plugin@git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git#master",
"test:all": "npm run lint && tsc && npm run test:web && npm run test:mobile && ./scripts/test-safety-check.sh && echo '\n\n\nGotta add the performance tests'",
"test:all": "npm run lint && tsc && npm run test:unit && npm run test:web && npm run test:mobile && ./scripts/test-safety-check.sh && echo '\n\n\nGotta add the performance tests'",
"test:web": "npx playwright test -c playwright.config-local.ts --trace on",
"test:mobile": "./scripts/test-mobile.sh",
"test:android": "node scripts/test-android.js",
+21
View File
@@ -63,23 +63,44 @@ export const deepLinkPathSchemas = {
did: z.object({
did: z.string().optional(),
}),
discover: z.object({}),
help: z.object({}),
"help-notification-types": z.object({}),
"help-notifications": z.object({}),
"help-onboarding": z.object({}),
"help-terms": z.object({}),
"invite-one": z.object({}),
"invite-one-accept": z.object({
// optional because A) it could be a query param, and B) the page displays an input if things go wrong
jwt: z.string().optional(),
}),
"new-activity": z.object({}),
"onboard-meeting-list": z.object({}),
"onboard-meeting-members": z.object({
groupId: z.string(),
}),
project: z.object({
id: z.string(),
}),
projects: z.object({}),
"recent-offers-to-user": z.object({}),
"recent-offers-to-user-projects": z.object({}),
"search-area": z.object({}),
"share-my-contact-info": z.object({}),
statistics: z.object({}),
"user-profile": z.object({
id: z.string(),
}),
};
export const deepLinkQuerySchemas = {
// All optional: the view defaults searchText to "" and the two flags to
// false, so a bare "timesafari://discover" is valid.
discover: z.object({
searchText: z.string().optional(),
searchPeople: z.string().optional(),
hideOnboarding: z.string().optional(),
}),
"onboard-meeting-members": z.object({
password: z.string(),
}),
-5
View File
@@ -33,11 +33,6 @@ const routes: Array<RouteRecordRaw> = [
name: "claim-cert",
component: () => import("../views/ClaimCertificateView.vue"),
},
{
path: "/confirm-contact",
name: "confirm-contact",
component: () => import("../views/ConfirmContactView.vue"),
},
{
path: "/confirm-gift/:id?",
name: "confirm-gift",
+57
View File
@@ -0,0 +1,57 @@
import { readFileSync } from "fs";
import { join } from "path";
import { deepLinkPathSchemas } from "@/interfaces/deepLinks";
/**
* Every deep link path must name a route the router actually serves.
*
* The two lists are maintained by hand in separate files, so nothing but this
* test stops them drifting apart. A path that names a missing route reaches the
* user as a link that validates, navigates, and lands nowhere.
*
* The router is read as text rather than imported: it builds a real Router at
* module scope and pulls in every view, which Jest's node environment cannot
* load.
*/
const ROUTER_SOURCE = join(__dirname, "..", "router", "index.ts");
/** Deep link paths that intentionally resolve to a differently named route. */
const ALIASES: Record<string, string> = {
// Must match ROUTE_NAME_RESOLVERS in src/services/deepLinks.ts
"": "home",
};
function routerRouteNames(): Set<string> {
const source = readFileSync(ROUTER_SOURCE, "utf8");
const names = [...source.matchAll(/^\s+name: "([^"]+)",$/gm)].map(
(match) => match[1],
);
return new Set(names);
}
describe("deep link paths", () => {
const routeNames = routerRouteNames();
it("parses the router source", () => {
// Guards against a silent pass if the router's formatting changes and the
// pattern above stops matching.
expect(routeNames.size).toBeGreaterThan(40);
expect(routeNames.has("home")).toBe(true);
});
it.each(Object.keys(deepLinkPathSchemas))(
'path "%s" resolves to a real route',
(path) => {
expect(routeNames).toContain(ALIASES[path] ?? path);
},
);
it("aliases only paths that need one", () => {
for (const path of Object.keys(ALIASES)) {
expect(Object.keys(deepLinkPathSchemas)).toContain(path);
expect(routeNames.has(path)).toBe(false);
}
});
});
-66
View File
@@ -1,66 +0,0 @@
<template>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Sub View Heading -->
<div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 class="grow text-xl text-center font-semibold leading-tight">
Confirm Contact
</h1>
<!-- Back -->
<router-link
class="order-first text-lg text-center leading-none p-1"
:to="{ name: 'account' }"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</router-link>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div>
<p class="text-center text-xl mb-4 font-light">
Would you like to add <i>Firstname</i> to your network?
</p>
<!-- Account Details -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<h2 class="text-xl font-semibold mb-2">Firstname Lastname</h2>
<div class="text-slate-500 text-sm font-bold">ID</div>
<div class="text-sm text-slate-500 mb-1">
<span><code>did:peer:kl45kj41lk451kl3</code></span>
</div>
</div>
<div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<input
type="submit"
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
value="Add Contact"
/>
<button
type="button"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
>
Cancel
</button>
</div>
</div>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
@Component({
components: {},
})
export default class ConfirmContactView extends Vue {}
</script>
+10 -4
View File
@@ -813,6 +813,16 @@ export default class ProjectViewView extends Vue {
async created() {
this.notify = createNotifyHelpers(this.$notify);
// Read the project id before the first await. The offer and gift buttons
// render as soon as activeDid and isRegistered are set below, and they hand
// 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.
const pathParam = window.location.pathname.substring("/project/".length);
if (pathParam) {
this.projectId = decodeURIComponent(pathParam);
}
const settings = await this.$accountSettings();
// Get activeDid from active_identity table (single source of truth)
@@ -838,10 +848,6 @@ export default class ProjectViewView extends Vue {
);
}
const pathParam = window.location.pathname.substring("/project/".length);
if (pathParam) {
this.projectId = decodeURIComponent(pathParam);
}
this.loadProject(this.projectId, this.activeDid);
this.loadTotals();
+9 -3
View File
@@ -20,7 +20,12 @@ test('Record an offer', async ({ page }) => {
await page.getByTestId('closeOnboardingAndFinish').click();
await page.locator('ul#listDiscoverResults li:nth-child(1)').click();
// Record an offer
await page.locator('button', { hasText: 'Edit' }).isVisible(); // since the 'edit' takes longer to show, wait for that (lest the click miss)
// The line that used to sit here waited for a button with the text 'Edit'. It
// never waited (isVisible() returns at once) and never matched (that button
// carries title="Edit", no text, and renders only for the project's issuer).
// The project id is read from the URL, so wait for the navigation to land.
await page.waitForURL(/\/project\//);
await expect(page.getByTestId('offerButton')).toBeVisible();
await page.getByTestId('offerButton').click();
await page.getByTestId('inputDescription').fill(description);
await page.getByTestId('inputOfferAmount').locator('input').fill(randomNonZeroNumber.toString());
@@ -48,8 +53,9 @@ test('Record an offer', async ({ page }) => {
await page.getByRole('link', { name: 'Offers', exact: true }).click();
await page.locator('li').filter({ hasText: description }).locator('a').first().click();
await page.getByTestId('editClaimButton').click();
await page.locator('heading', { hasText: 'What is offered' }).isVisible();
const itemDesc = await page.getByTestId('itemDescription');
// ('heading' is not an element name and isVisible() does not wait, so the line
// that used to sit here matched nothing; toHaveValue below polls properly.)
const itemDesc = page.getByTestId('itemDescription');
await expect(itemDesc).toHaveValue(description);
const amount = await page.getByTestId('inputOfferAmount');
await expect(amount).toHaveValue(randomNonZeroNumber.toString());