add some other deep links for useful pages, and fix a UI test
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user